diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4725759858..60c9958501 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -19,8 +19,26 @@ bitcoin = { version = "0.30.0", features = ["rand"] } libfuzzer-sys = "0.4" ord = { path = ".." } +[[bin]] +name = "runestone-decipher" +path = "fuzz_targets/runestone_decipher.rs" +test = false +doc = false + [[bin]] name = "transaction-builder" path = "fuzz_targets/transaction_builder.rs" test = false doc = false + +[[bin]] +name = "varint-encode" +path = "fuzz_targets/varint_encode.rs" +test = false +doc = false + +[[bin]] +name = "varint-decode" +path = "fuzz_targets/varint_decode.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/runestone_decipher.rs b/fuzz/fuzz_targets/runestone_decipher.rs new file mode 100644 index 0000000000..43cdb315aa --- /dev/null +++ b/fuzz/fuzz_targets/runestone_decipher.rs @@ -0,0 +1,36 @@ +#![no_main] + +use { + bitcoin::{ + locktime, opcodes, + script::{self, PushBytes}, + Transaction, TxOut, + }, + libfuzzer_sys::fuzz_target, + ord::runes::Runestone, +}; + +fuzz_target!(|input: Vec>| { + let mut builder = script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"RUNE_TEST"); + + for slice in input { + let Ok(push): Result<&PushBytes, _> = slice.as_slice().try_into() else { + continue; + }; + builder = builder.push_slice(push); + } + + let tx = Transaction { + input: Vec::new(), + lock_time: locktime::absolute::LockTime::ZERO, + output: vec![TxOut { + script_pubkey: builder.into_script(), + value: 0, + }], + version: 0, + }; + + Runestone::from_transaction(&tx); +}); diff --git a/fuzz/fuzz_targets/transaction_builder.rs b/fuzz/fuzz_targets/transaction_builder.rs index ad1be3dcfe..e91f5af2d3 100644 --- a/fuzz/fuzz_targets/transaction_builder.rs +++ b/fuzz/fuzz_targets/transaction_builder.rs @@ -7,7 +7,7 @@ use { Amount, OutPoint, }, libfuzzer_sys::fuzz_target, - ord::{FeeRate, SatPoint, TransactionBuilder}, + ord::{FeeRate, SatPoint, Target, TransactionBuilder}, std::collections::BTreeMap, }; @@ -62,29 +62,34 @@ fuzz_target!(|input: Input| { .assume_checked(), ]; - let Ok(fee_rate) = FeeRate::try_from(input.fee_rate) else { return; }; + let Ok(fee_rate) = FeeRate::try_from(input.fee_rate) else { + return; + }; match input.output_value { Some(output_value) => { - let _ = TransactionBuilder::build_transaction_with_value( + let _ = TransactionBuilder::new( satpoint, inscriptions, amounts, recipient, change, fee_rate, - Amount::from_sat(output_value), - ); + Target::Value(Amount::from_sat(output_value)), + ) + .build_transaction(); } None => { - let _ = TransactionBuilder::build_transaction_with_postage( + let _ = TransactionBuilder::new( satpoint, inscriptions, amounts, recipient, change, fee_rate, - ); + Target::Postage, + ) + .build_transaction(); } } }); diff --git a/fuzz/fuzz_targets/varint_decode.rs b/fuzz/fuzz_targets/varint_decode.rs new file mode 100644 index 0000000000..b7022c7241 --- /dev/null +++ b/fuzz/fuzz_targets/varint_decode.rs @@ -0,0 +1,17 @@ +#![no_main] + +use {libfuzzer_sys::fuzz_target, ord::runes::varint}; + +fuzz_target!(|input: &[u8]| { + let mut i = 0; + + while i < input.len() { + let Ok((decoded, length)) = varint::decode(&input[i..]) else { + break; + }; + let mut encoded = Vec::new(); + varint::encode_to_vec(decoded, &mut encoded); + assert_eq!(encoded, &input[i..i + length]); + i += length; + } +}); diff --git a/fuzz/fuzz_targets/varint_encode.rs b/fuzz/fuzz_targets/varint_encode.rs new file mode 100644 index 0000000000..b89c322562 --- /dev/null +++ b/fuzz/fuzz_targets/varint_encode.rs @@ -0,0 +1,11 @@ +#![no_main] + +use {libfuzzer_sys::fuzz_target, ord::runes::varint}; + +fuzz_target!(|input: u128| { + let mut encoded = Vec::new(); + varint::encode_to_vec(input, &mut encoded); + let (decoded, length) = varint::decode(&encoded).unwrap(); + assert_eq!(length, encoded.len()); + assert_eq!(decoded, input); +}); diff --git a/justfile b/justfile index b5739fc00d..938f294481 100644 --- a/justfile +++ b/justfile @@ -68,7 +68,17 @@ profile-tests: | tee test-times.txt fuzz: - cd fuzz && cargo +nightly fuzz run transaction-builder + #!/usr/bin/env bash + set -euxo pipefail + + cd fuzz + + while true; do + cargo +nightly fuzz run transaction-builder -- -max_total_time=60 + cargo +nightly fuzz run runestone-decipher -- -max_total_time=60 + cargo +nightly fuzz run varint-decode -- -max_total_time=60 + cargo +nightly fuzz run varint-encode -- -max_total_time=60 + done open: open http://localhost diff --git a/src/index.rs b/src/index.rs index 5901c9af9a..4a90797fea 100644 --- a/src/index.rs +++ b/src/index.rs @@ -35,7 +35,7 @@ mod updater; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 8; +const SCHEMA_VERSION: u64 = 9; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { @@ -75,16 +75,17 @@ pub enum List { } #[derive(Copy, Clone)] -#[repr(u64)] pub(crate) enum Statistic { - Schema = 0, - Commits = 1, - LostSats = 2, - OutputsTraversed = 3, - SatRanges = 4, - UnboundInscriptions = 5, - CursedInscriptions = 6, - BlessedInscriptions = 7, + BlessedInscriptions, + Commits, + CursedInscriptions, + IndexRunes, + IndexSats, + LostSats, + OutputsTraversed, + SatRanges, + Schema, + UnboundInscriptions, } impl Statistic { @@ -154,6 +155,8 @@ pub(crate) struct Index { genesis_block_coinbase_transaction: Transaction, genesis_block_coinbase_txid: Txid, height_limit: Option, + index_runes: bool, + index_sats: bool, options: Options, path: PathBuf, unrecoverably_reorged: AtomicBool, @@ -207,19 +210,23 @@ impl Index { redb::Durability::Immediate }; + let index_runes; + let index_sats; + let database = match Database::builder() .set_cache_size(db_cache_size) .open(&path) { Ok(database) => { - let schema_version = database - .begin_read()? - .open_table(STATISTIC_TO_COUNT)? - .get(&Statistic::Schema.key())? - .map(|x| x.value()) - .unwrap_or(0); - - match schema_version.cmp(&SCHEMA_VERSION) { + { + let tx = database.begin_read()?; + let schema_version = tx + .open_table(STATISTIC_TO_COUNT)? + .get(&Statistic::Schema.key())? + .map(|x| x.value()) + .unwrap_or(0); + + match schema_version.cmp(&SCHEMA_VERSION) { cmp::Ordering::Less => bail!( "index at `{}` appears to have been built with an older, incompatible version of ord, consider deleting and rebuilding the index: index schema {schema_version}, ord schema {SCHEMA_VERSION}", @@ -234,6 +241,20 @@ impl Index { } } + let statistics = tx.open_table(STATISTIC_TO_COUNT)?; + + index_runes = statistics + .get(&Statistic::IndexRunes.key())? + .unwrap() + .value() + != 0; + index_sats = statistics + .get(&Statistic::IndexSats.key())? + .unwrap() + .value() + != 0; + } + database } Err(_) => { @@ -253,23 +274,31 @@ impl Index { tx.open_table(INSCRIPTION_ID_TO_INSCRIPTION_ENTRY)?; tx.open_table(INSCRIPTION_ID_TO_SATPOINT)?; tx.open_table(INSCRIPTION_NUMBER_TO_INSCRIPTION_ID)?; + tx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; tx.open_table(OUTPOINT_TO_VALUE)?; + tx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; + tx.open_table(RUNE_TO_RUNE_ID)?; tx.open_table(SAT_TO_SATPOINT)?; tx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ID)?; tx.open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)?; - tx.open_table(STATISTIC_TO_COUNT)? - .insert(&Statistic::Schema.key(), &SCHEMA_VERSION)?; + { + let mut outpoint_to_sat_ranges = tx.open_table(OUTPOINT_TO_SAT_RANGES)?; + let mut statistics = tx.open_table(STATISTIC_TO_COUNT)?; - if options.index_runes() { - tx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; - tx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; - tx.open_table(RUNE_TO_RUNE_ID)?; - } + if options.index_sats { + outpoint_to_sat_ranges.insert(&OutPoint::null().store(), [].as_slice())?; + } - if options.index_sats { - tx.open_table(OUTPOINT_TO_SAT_RANGES)? - .insert(&OutPoint::null().store(), [].as_slice())?; + index_runes = options.index_runes(); + index_sats = options.index_sats; + + statistics.insert( + &Statistic::IndexRunes.key(), + &u64::from(options.index_runes()), + )?; + statistics.insert(&Statistic::IndexSats.key(), &u64::from(options.index_sats))?; + statistics.insert(&Statistic::Schema.key(), &SCHEMA_VERSION)?; } tx.commit()?; @@ -290,6 +319,8 @@ impl Index { genesis_block_coinbase_transaction, height_limit: options.height_limit, options: options.clone(), + index_runes, + index_sats, path, unrecoverably_reorged: AtomicBool::new(false), }) @@ -358,28 +389,8 @@ impl Index { .collect() } - pub(crate) fn has_rune_index(&self) -> Result { - match self.begin_read()?.0.open_table(RUNE_ID_TO_RUNE_ENTRY) { - Ok(_) => Ok(true), - Err(redb::TableError::TableDoesNotExist(_)) => Ok(false), - Err(err) => Err(err.into()), - } - } - - pub(crate) fn has_sat_index(&self) -> Result { - match self.begin_read()?.0.open_table(OUTPOINT_TO_SAT_RANGES) { - Ok(_) => Ok(true), - Err(redb::TableError::TableDoesNotExist(_)) => Ok(false), - Err(err) => Err(err.into()), - } - } - - fn require_sat_index(&self, feature: &str) -> Result { - if !self.has_sat_index()? { - bail!("{feature} requires index created with `--index-sats` flag") - } - - Ok(()) + pub(crate) fn has_sat_index(&self) -> bool { + self.index_sats } pub(crate) fn info(&self) -> Result { @@ -602,86 +613,77 @@ impl Index { Ok(blocks) } - pub(crate) fn rare_sat_satpoints(&self) -> Result>> { - if self.has_sat_index()? { - let rtx = self.database.begin_read()?; - - let sat_to_satpoint = rtx.open_table(SAT_TO_SATPOINT)?; + pub(crate) fn rare_sat_satpoints(&self) -> Result> { + let rtx = self.database.begin_read()?; - let mut result = Vec::with_capacity(sat_to_satpoint.len()?.try_into().unwrap()); + let sat_to_satpoint = rtx.open_table(SAT_TO_SATPOINT)?; - for range in sat_to_satpoint.range(0..)? { - let (sat, satpoint) = range?; - result.push((Sat(sat.value()), Entry::load(*satpoint.value()))); - } + let mut result = Vec::with_capacity(sat_to_satpoint.len()?.try_into().unwrap()); - Ok(Some(result)) - } else { - Ok(None) + for range in sat_to_satpoint.range(0..)? { + let (sat, satpoint) = range?; + result.push((Sat(sat.value()), Entry::load(*satpoint.value()))); } + + Ok(result) } pub(crate) fn rare_sat_satpoint(&self, sat: Sat) -> Result> { - if self.has_sat_index()? { - Ok( - self - .database - .begin_read()? - .open_table(SAT_TO_SATPOINT)? - .get(&sat.n())? - .map(|satpoint| Entry::load(*satpoint.value())), - ) - } else { - Ok(None) - } + Ok( + self + .database + .begin_read()? + .open_table(SAT_TO_SATPOINT)? + .get(&sat.n())? + .map(|satpoint| Entry::load(*satpoint.value())), + ) } - pub(crate) fn rune(&self, rune: Rune) -> Result> { - if self.has_rune_index()? { - let rtx = self.database.begin_read()?; - - let entry = match rtx.open_table(RUNE_TO_RUNE_ID)?.get(rune.0)? { - Some(id) => rtx - .open_table(RUNE_ID_TO_RUNE_ENTRY)? - .get(id.value())? - .map(|entry| (RuneId::load(id.value()), RuneEntry::load(entry.value()))), - None => None, - }; - - Ok(entry) - } else { - Ok(None) - } + pub(crate) fn get_rune_by_id(&self, id: RuneId) -> Result> { + Ok( + self + .database + .begin_read()? + .open_table(RUNE_ID_TO_RUNE_ENTRY)? + .get(&id.store())? + .map(|entry| RuneEntry::load(entry.value()).rune), + ) } - pub(crate) fn runes(&self) -> Result>> { - if self.has_rune_index()? { - let mut entries = Vec::new(); + pub(crate) fn rune(&self, rune: Rune) -> Result> { + let rtx = self.database.begin_read()?; - for result in self - .database - .begin_read()? + let entry = match rtx.open_table(RUNE_TO_RUNE_ID)?.get(rune.0)? { + Some(id) => rtx .open_table(RUNE_ID_TO_RUNE_ENTRY)? - .iter()? - { - let (id, entry) = result?; - entries.push((RuneId::load(id.value()), RuneEntry::load(entry.value()))); - } + .get(id.value())? + .map(|entry| (RuneId::load(id.value()), RuneEntry::load(entry.value()))), + None => None, + }; - Ok(Some(entries)) - } else { - Ok(None) + Ok(entry) + } + + pub(crate) fn runes(&self) -> Result> { + let mut entries = Vec::new(); + + for result in self + .database + .begin_read()? + .open_table(RUNE_ID_TO_RUNE_ENTRY)? + .iter()? + { + let (id, entry) = result?; + entries.push((RuneId::load(id.value()), RuneEntry::load(entry.value()))); } + + Ok(entries) } pub(crate) fn get_rune_balances_for_outpoint( &self, outpoint: OutPoint, ) -> Result> { - if !self.has_rune_index()? { - return Ok(Vec::new()); - } - let rtx = &self.database.begin_read()?; let outpoint_to_balances = rtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; @@ -964,8 +966,6 @@ impl Index { } pub(crate) fn find(&self, sat: u64) -> Result> { - self.require_sat_index("find")?; - let rtx = self.begin_read()?; if rtx.block_count()? <= Sat(sat).height().n() { @@ -997,8 +997,6 @@ impl Index { range_start: u64, range_end: u64, ) -> Result>> { - self.require_sat_index("find")?; - let rtx = self.begin_read()?; if rtx.block_count()? < Sat(range_end - 1).height().n() + 1 { @@ -1057,7 +1055,9 @@ impl Index { } pub(crate) fn list(&self, outpoint: OutPoint) -> Result> { - self.require_sat_index("list")?; + if !self.index_sats { + return Ok(None); + } let array = outpoint.store(); @@ -1301,7 +1301,7 @@ impl Index { match sat { Some(sat) => { - if self.has_sat_index().unwrap() { + if self.index_sats { // unbound inscriptions should not be assigned to a sat assert!(satpoint.outpoint != unbound_outpoint()); assert!(rtx @@ -1329,7 +1329,7 @@ impl Index { } } None => { - if self.has_sat_index().unwrap() { + if self.index_sats { assert!(satpoint.outpoint == unbound_outpoint()) } } @@ -1378,19 +1378,17 @@ impl Index { } } - if self.has_sat_index().unwrap() { - for range in rtx - .open_multimap_table(SAT_TO_INSCRIPTION_ID) - .unwrap() - .iter() - .into_iter() - { - for entry in range.into_iter() { - let (_sat, ids) = entry.unwrap(); - assert!(!ids - .into_iter() - .any(|id| InscriptionId::load(*id.unwrap().value()) == inscription_id)) - } + for range in rtx + .open_multimap_table(SAT_TO_INSCRIPTION_ID) + .unwrap() + .iter() + .into_iter() + { + for entry in range.into_iter() { + let (_sat, ids) = entry.unwrap(); + assert!(!ids + .into_iter() + .any(|id| InscriptionId::load(*id.unwrap().value()) == inscription_id)) } } } diff --git a/src/index/entry.rs b/src/index/entry.rs index abc63de3fd..3ec8afff72 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -32,6 +32,19 @@ pub(crate) struct RuneEntry { pub(crate) symbol: Option, } +impl Default for RuneEntry { + fn default() -> Self { + Self { + burned: 0, + divisibility: 0, + etching: Txid::all_zeros(), + rune: Rune(0), + supply: 0, + symbol: None, + } + } +} + pub(super) type RuneEntryValue = (u128, u8, (u128, u128), u128, u128, u32); impl Entry for RuneEntry { diff --git a/src/index/testing.rs b/src/index/testing.rs index 1948cc2d3d..1dbf0f3b35 100644 --- a/src/index/testing.rs +++ b/src/index/testing.rs @@ -53,11 +53,6 @@ impl ContextBuilder { self } - pub(crate) fn chain(mut self, chain: Chain) -> Self { - self.chain = chain; - self - } - pub(crate) fn tempdir(mut self, tempdir: TempDir) -> Self { self.tempdir = Some(tempdir); self diff --git a/src/index/updater.rs b/src/index/updater.rs index 11fd03c8cc..03f5d51411 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -34,7 +34,6 @@ pub(crate) struct Updater<'index> { range_cache: HashMap>, height: u64, index: &'index Index, - index_sats: bool, sat_ranges_since_flush: u64, outputs_cached: u64, outputs_inserted_since_flush: u64, @@ -47,7 +46,6 @@ impl<'index> Updater<'_> { range_cache: HashMap::new(), height: index.block_count()?, index, - index_sats: index.has_sat_index()?, sat_ranges_since_flush: 0, outputs_cached: 0, outputs_inserted_since_flush: 0, @@ -84,7 +82,7 @@ impl<'index> Updater<'_> { Some(progress_bar) }; - let rx = Self::fetch_blocks_from(self.index, self.height, self.index_sats)?; + let rx = Self::fetch_blocks_from(self.index, self.height, self.index.index_sats)?; let (mut outpoint_sender, mut value_receiver) = Self::spawn_fetcher(self.index)?; @@ -378,7 +376,7 @@ impl<'index> Updater<'_> { } } - if index.options.index_runes() { + if index.index_runes { let mut outpoint_to_rune_balances = wtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; let mut rune_id_to_rune_entry = wtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; let mut rune_to_rune_id = wtx.open_table(RUNE_TO_RUNE_ID)?; @@ -446,7 +444,7 @@ impl<'index> Updater<'_> { value_cache, )?; - if self.index_sats { + if self.index.index_sats { let mut sat_to_satpoint = wtx.open_table(SAT_TO_SATPOINT)?; let mut outpoint_to_sat_ranges = wtx.open_table(OUTPOINT_TO_SAT_RANGES)?; @@ -668,7 +666,7 @@ impl<'index> Updater<'_> { self.outputs_cached ); - if self.index_sats { + if self.index.index_sats { log::info!( "Flushing {} entries ({:.1}% resulting from {} insertions) from memory to database", self.range_cache.len(), diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 2c2c2f11be..bbca38911a 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -59,6 +59,11 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { } } + let burn = runestone + .as_ref() + .map(|runestone| runestone.burn) + .unwrap_or_default(); + // A vector of allocated transaction output rune balances let mut allocated: Vec> = vec![HashMap::new(); tx.output.len()]; @@ -90,42 +95,75 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { None => None, }; - for Edict { id, amount, output } in runestone.edicts { - // Skip edicts not referring to valid outputs - if output >= tx.output.len() as u128 { - continue; - } + if !burn { + for Edict { id, amount, output } in runestone.edicts { + let Ok(output) = usize::try_from(output) else { + continue; + }; - let (balance, id) = if id == 0 { - // 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 allocation.as_mut() { - Some(Allocation { balance, id, .. }) => (balance, *id), - None => continue, + // Skip edicts not referring to valid outputs + if output > tx.output.len() { + continue; } - } else { - // Get the unallocated balance of the given ID - match unallocated.get_mut(&id) { - Some(balance) => (balance, id), - None => continue, + + let (balance, id) = if id == 0 { + // 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 allocation.as_mut() { + Some(Allocation { balance, id, .. }) => (balance, *id), + None => continue, + } + } else { + // Get the unallocated balance of the given ID + match unallocated.get_mut(&id) { + Some(balance) => (balance, id), + None => continue, + } + }; + + let mut allocate = |balance: &mut u128, amount: u128, output: usize| { + if amount > 0 { + *balance -= amount; + *allocated[output].entry(id).or_default() += amount; + } + }; + + if output == tx.output.len() { + // find non-OP_RETURN outputs + let destinations = tx + .output + .iter() + .enumerate() + .filter_map(|(output, tx_out)| { + (!tx_out.script_pubkey.is_op_return()).then_some(output) + }) + .collect::>(); + + if amount == 0 { + // if amount is zero, divide balance between eligible outputs + let amount = *balance / destinations.len() as u128; + + for output in destinations { + allocate(balance, amount, output); + } + } else { + // if amount is non-zero, distribute amount to eligible outputs + for output in destinations { + allocate(balance, amount.min(*balance), output); + } + } + } else { + // Get the allocatable amount + let amount = if amount == 0 { + *balance + } else { + amount.min(*balance) + }; + + allocate(balance, amount, output); } - }; - - // Get the allocatable amount - let amount = if amount == 0 { - *balance - } else { - amount.min(*balance) - }; - - // If the amount to be allocated is greater than zero, - // deduct it from the remaining balance, and increment - // the allocated entry. - if amount > 0 { - *balance -= amount; - *allocated[output as usize].entry(id).or_default() += amount; } } @@ -154,22 +192,28 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { } } - // Assign all un-allocated runes to the first non OP_RETURN output - if let Some((vout, _)) = tx - .output - .iter() - .enumerate() - .find(|(_, tx_out)| !tx_out.script_pubkey.is_op_return()) - { + let mut burned: HashMap = HashMap::new(); + + if burn { for (id, balance) in unallocated { - if balance > 0 { - *allocated[vout].entry(id).or_default() += balance; + *burned.entry(id).or_default() += balance; + } + } else { + // Assign all un-allocated runes to the first non OP_RETURN output + if let Some((vout, _)) = tx + .output + .iter() + .enumerate() + .find(|(_, tx_out)| !tx_out.script_pubkey.is_op_return()) + { + for (id, balance) in unallocated { + if balance > 0 { + *allocated[vout].entry(id).or_default() += balance; + } } } } - let mut burned: HashMap = HashMap::new(); - // update outpoint balances let mut buffer: Vec = Vec::new(); for (vout, balances) in allocated.into_iter().enumerate() { diff --git a/src/lib.rs b/src/lib.rs index 7ccc0b30f8..d5f4d49d68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ use { serde::{Deserialize, Deserializer, Serialize, Serializer}, std::{ cmp, - collections::{BTreeMap, HashSet, VecDeque}, + collections::{BTreeMap, HashMap, HashSet, VecDeque}, env, ffi::OsString, fmt::{self, Display, Formatter}, @@ -79,8 +79,13 @@ use { }; pub use crate::{ - fee_rate::FeeRate, inscription::Inscription, object::Object, rarity::Rarity, sat::Sat, - sat_point::SatPoint, subcommand::wallet::transaction_builder::TransactionBuilder, + fee_rate::FeeRate, + inscription::Inscription, + object::Object, + rarity::Rarity, + sat::Sat, + sat_point::SatPoint, + subcommand::wallet::transaction_builder::{Target, TransactionBuilder}, }; #[cfg(test)] @@ -121,7 +126,7 @@ mod outgoing; mod page_config; pub mod rarity; mod representation; -mod runes; +pub mod runes; pub mod sat; mod sat_point; pub mod subcommand; diff --git a/src/page_config.rs b/src/page_config.rs index c6510f2ca1..0bc36cb5cc 100644 --- a/src/page_config.rs +++ b/src/page_config.rs @@ -4,4 +4,5 @@ use super::*; pub(crate) struct PageConfig { pub(crate) chain: Chain, pub(crate) domain: Option, + pub(crate) index_sats: bool, } diff --git a/src/runes.rs b/src/runes.rs index 72bbe5b360..228cc27783 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -1,8 +1,8 @@ use {self::error::Error, super::*}; -pub(crate) use { - edict::Edict, etching::Etching, pile::Pile, rune::Rune, rune_id::RuneId, runestone::Runestone, -}; +pub use runestone::Runestone; + +pub(crate) use {edict::Edict, etching::Etching, pile::Pile, rune::Rune, rune_id::RuneId}; const MAX_DIVISIBILITY: u8 = 38; @@ -13,7 +13,7 @@ mod pile; mod rune; mod rune_id; mod runestone; -pub(crate) mod varint; +pub mod varint; type Result = std::result::Result; @@ -23,38 +23,18 @@ mod tests { const RUNE: u128 = 99246114928149462; - #[test] - fn index_only_indexes_runes_if_flag_is_passed_and_on_mainnet() { - assert!(!Context::builder().build().index.has_rune_index().unwrap()); - assert!(!Context::builder() - .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") - .chain(Chain::Mainnet) - .build() - .index - .has_rune_index() - .unwrap()); - assert!(Context::builder() - .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") - .build() - .index - .has_rune_index() - .unwrap()); - } - #[test] fn index_starts_with_no_runes() { let context = Context::builder() .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") .build(); - assert_eq!(context.index.runes().unwrap().unwrap(), []); + assert_eq!(context.index.runes().unwrap(), []); assert_eq!(context.index.get_rune_balances(), []); } #[test] - fn empty_runestone_does_not_create_rune() { - let context = Context::builder() - .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") - .build(); + fn default_index_does_not_index_runes() { + let context = Context::builder().build(); context.mine_blocks(1); @@ -62,8 +42,17 @@ mod tests { inputs: &[(1, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: Vec::new(), - etching: None, + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + divisibility: 0, + rune: Rune(RUNE), + symbol: None, + }), + ..Default::default() } .encipher(), ), @@ -72,7 +61,28 @@ mod tests { context.mine_blocks(1); - assert_eq!(context.index.runes().unwrap().unwrap(), []); + assert_eq!(context.index.runes().unwrap(), []); + + assert_eq!(context.index.get_rune_balances(), []); + } + + #[test] + fn empty_runestone_does_not_create_rune() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some(Runestone::default().encipher()), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!(context.index.runes().unwrap(), []); assert_eq!(context.index.get_rune_balances(), []); } @@ -88,12 +98,11 @@ mod tests { inputs: &[(1, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: Vec::new(), etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -108,16 +117,13 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), - supply: 0, - symbol: None, + ..Default::default() } )] ); @@ -143,10 +149,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -161,16 +167,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -200,10 +204,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE - 1), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -212,7 +216,7 @@ mod tests { context.mine_blocks(1); - assert_eq!(context.index.runes().unwrap().unwrap(), []); + assert_eq!(context.index.runes().unwrap(), []); assert_eq!(context.index.get_rune_balances(), []); } @@ -234,10 +238,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -252,16 +256,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -293,8 +295,9 @@ mod tests { etching: Some(Etching { divisibility: 1, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -309,16 +312,15 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, rune: Rune(RUNE), etching: txid, divisibility: 1, supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -354,10 +356,10 @@ mod tests { }, ], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -372,16 +374,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -417,10 +417,10 @@ mod tests { }, ], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -435,16 +435,15 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), symbol: None, + ..Default::default() } )] ); @@ -473,10 +472,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -491,16 +490,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: 100, - symbol: None, + ..Default::default() } )] ); @@ -536,10 +533,10 @@ mod tests { }, ], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -554,16 +551,15 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { burned: 100, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: 200, - symbol: None, + ..Default::default() } )] ); @@ -602,10 +598,10 @@ mod tests { }, ], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -620,16 +616,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: 100, - symbol: None, + ..Default::default() } )] ); @@ -658,10 +652,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -676,16 +670,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -710,7 +702,7 @@ mod tests { amount: u128::max_value(), output: 0, }], - etching: None, + ..Default::default() } .encipher(), ), @@ -720,16 +712,14 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -747,7 +737,7 @@ mod tests { } #[test] - fn unallocated_runes_are_assigned_to_first_non_op_return_output() { + fn etched_rune_is_burned_if_an_unrecognized_even_tag_is_encountered() { let context = Context::builder() .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") .build(); @@ -764,10 +754,60 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() + }), + burn: true, + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + ..Default::default() + } + )] + ); + + assert_eq!(context.index.get_rune_balances(), []); + } + + #[test] + fn input_runes_are_burned_if_an_unrecognized_even_tag_is_encountered() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -782,16 +822,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -807,12 +845,12 @@ mod tests { )] ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(2, 1, 0, Witness::new())], op_return: Some( Runestone { - edicts: Vec::new(), - etching: None, + burn: true, + ..Default::default() } .encipher(), ), @@ -822,16 +860,98 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, + burned: u128::max_value(), etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() + } + )] + ); + + assert_eq!(context.index.get_rune_balances(), []); + } + + #[test] + fn unallocated_runes_are_assigned_to_first_non_op_return_output() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [( + OutPoint { + txid: txid0, + vout: 0 + }, + vec![(id, u128::max_value())] + )] + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + op_return: Some(Runestone::default().encipher()), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() } )] ); @@ -867,10 +987,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -885,16 +1005,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -919,16 +1037,14 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -963,10 +1079,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -981,16 +1097,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -1010,10 +1124,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1023,16 +1137,14 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -1061,10 +1173,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1079,16 +1191,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -1114,10 +1224,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE + 1), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1132,28 +1242,24 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -1164,17 +1270,17 @@ mod tests { [ ( OutPoint { - txid: txid1, + txid: txid0, vout: 0 }, - vec![(id1, u128::max_value())] + vec![(id0, u128::max_value())] ), ( OutPoint { - txid: txid0, + txid: txid1, vout: 0 }, - vec![(id0, u128::max_value())] + vec![(id1, u128::max_value())] ), ] ); @@ -1187,28 +1293,24 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -1244,10 +1346,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1262,16 +1364,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -1297,10 +1397,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE + 1), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1315,28 +1415,24 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -1347,17 +1443,17 @@ mod tests { [ ( OutPoint { - txid: txid1, + txid: txid0, vout: 0 }, - vec![(id1, u128::max_value())] + vec![(id0, u128::max_value())] ), ( OutPoint { - txid: txid0, + txid: txid1, vout: 0 }, - vec![(id0, u128::max_value())] + vec![(id1, u128::max_value())] ), ] ); @@ -1370,28 +1466,24 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -1425,7 +1517,7 @@ mod tests { output: 1, }, ], - etching: None, + ..Default::default() } .encipher(), ), @@ -1435,28 +1527,24 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -1504,10 +1592,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1522,16 +1610,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -1557,10 +1643,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE + 1), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1575,28 +1661,24 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -1607,17 +1689,17 @@ mod tests { [ ( OutPoint { - txid: txid1, + txid: txid0, vout: 0 }, - vec![(id1, u128::max_value())] + vec![(id0, u128::max_value())] ), ( OutPoint { - txid: txid0, + txid: txid1, vout: 0 }, - vec![(id0, u128::max_value())] + vec![(id1, u128::max_value())] ), ] ); @@ -1638,7 +1720,7 @@ mod tests { output: 0, }, ], - etching: None, + ..Default::default() } .encipher(), ), @@ -1648,28 +1730,24 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -1706,10 +1784,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1724,16 +1802,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -1763,16 +1839,14 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -1801,10 +1875,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1826,10 +1900,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE + 1), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1844,28 +1918,24 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ] @@ -1876,17 +1946,17 @@ mod tests { [ ( OutPoint { - txid: txid1, + txid: txid0, vout: 0 }, - vec![(id1, u128::max_value())] + vec![(id0, u128::max_value())] ), ( OutPoint { - txid: txid0, + txid: txid1, vout: 0 }, - vec![(id0, u128::max_value())] + vec![(id1, u128::max_value())] ), ] ); @@ -1910,10 +1980,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -1928,16 +1998,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -1969,7 +2037,7 @@ mod tests { output: 0, }, ], - etching: None, + ..Default::default() } .encipher(), ), @@ -1979,16 +2047,14 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -2023,10 +2089,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2041,16 +2107,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -2076,10 +2140,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE + 1), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2094,28 +2158,24 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -2126,17 +2186,17 @@ mod tests { [ ( OutPoint { - txid: txid1, + txid: txid0, vout: 0 }, - vec![(id1, u128::max_value())] + vec![(id0, u128::max_value())] ), ( OutPoint { - txid: txid0, + txid: txid1, vout: 0 }, - vec![(id0, u128::max_value())] + vec![(id1, u128::max_value())] ), ] ); @@ -2157,7 +2217,7 @@ mod tests { output: 0, }, ], - etching: None, + ..Default::default() } .encipher(), ), @@ -2167,28 +2227,24 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [ ( id0, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } ), ( id1, RuneEntry { - burned: 0, - divisibility: 0, etching: txid1, rune: Rune(RUNE + 1), supply: u128::max_value(), - symbol: None, + ..Default::default() } ) ] @@ -2233,10 +2289,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2251,16 +2307,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value() / 2, - symbol: None, + ..Default::default() } )] ); @@ -2285,7 +2339,7 @@ mod tests { amount: u128::max_value(), output: 0, }], - etching: None, + ..Default::default() } .encipher(), ), @@ -2295,16 +2349,14 @@ mod tests { context.mine_blocks(1); assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid0, rune: Rune(RUNE), supply: u128::max_value() / 2, - symbol: None, + ..Default::default() } )] ); @@ -2339,10 +2391,10 @@ mod tests { output: 1, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2357,16 +2409,15 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { burned: u128::max_value(), - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -2396,10 +2447,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2414,16 +2465,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -2460,10 +2509,10 @@ mod tests { }, ], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2478,16 +2527,14 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -2499,7 +2546,7 @@ mod tests { } #[test] - fn etching_may_specify_symbol() { + fn split_in_etching() { let context = Context::builder() .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") .build(); @@ -2508,18 +2555,19 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Witness::new())], + outputs: 4, op_return: Some( Runestone { edicts: vec![Edict { id: 0, - amount: u128::max_value(), - output: 0, + amount: 0, + output: 5, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: Some('$'), + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2534,28 +2582,43 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), - supply: u128::max_value(), - symbol: Some('$'), + supply: (u128::max_value() / 4) * 4, + ..Default::default() } )] ); assert_eq!( context.index.get_rune_balances(), - [(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])] + [ + ( + OutPoint { txid, vout: 0 }, + vec![(id, u128::max_value() / 4)] + ), + ( + OutPoint { txid, vout: 1 }, + vec![(id, u128::max_value() / 4)] + ), + ( + OutPoint { txid, vout: 2 }, + vec![(id, u128::max_value() / 4)] + ), + ( + OutPoint { txid, vout: 3 }, + vec![(id, u128::max_value() / 4)] + ), + ] ); } #[test] - fn allocate_all_remaining_runes_in_etching() { + fn split_in_etching_with_preceding_edict() { let context = Context::builder() .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") .build(); @@ -2564,18 +2627,105 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Witness::new())], + outputs: 4, op_return: Some( Runestone { - edicts: vec![Edict { - id: 0, - amount: 0, - output: 0, - }], + edicts: vec![ + Edict { + id: 0, + amount: 1000, + output: 0, + }, + Edict { + id: 0, + amount: 0, + output: 5, + }, + ], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid, + rune: Rune(RUNE), + supply: 1000 + ((u128::max_value() - 1000) / 4) * 4, + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { txid, vout: 0 }, + vec![(id, 1000 + (u128::max_value() - 1000) / 4)] + ), + ( + OutPoint { txid, vout: 1 }, + vec![(id, (u128::max_value() - 1000) / 4)] + ), + ( + OutPoint { txid, vout: 2 }, + vec![(id, (u128::max_value() - 1000) / 4)] + ), + ( + OutPoint { txid, vout: 3 }, + vec![(id, (u128::max_value() - 1000) / 4)] + ), + ] + ); + } + + #[test] + fn split_in_etching_with_following_edict() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + outputs: 4, + op_return: Some( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 0, + output: 5, + }, + Edict { + id: 0, + amount: 1000, + output: 0, + }, + ], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2590,48 +2740,64 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); assert_eq!( context.index.get_rune_balances(), - [(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])] + [ + ( + OutPoint { txid, vout: 0 }, + vec![(id, u128::max_value() / 4 + 3)] + ), + ( + OutPoint { txid, vout: 1 }, + vec![(id, u128::max_value() / 4)] + ), + ( + OutPoint { txid, vout: 2 }, + vec![(id, u128::max_value() / 4)] + ), + ( + OutPoint { txid, vout: 3 }, + vec![(id, u128::max_value() / 4)] + ), + ] ); } #[test] - fn allocate_all_remaining_runes_in_inputs() { + fn split_with_amount_in_etching() { let context = Context::builder() .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") .build(); context.mine_blocks(1); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Witness::new())], + outputs: 4, op_return: Some( Runestone { edicts: vec![Edict { id: 0, - amount: u128::max_value(), - output: 0, + amount: 1000, + output: 5, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -2646,42 +2812,59 @@ mod tests { }; assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, - etching: txid0, + etching: txid, rune: Rune(RUNE), - supply: u128::max_value(), - symbol: None, + supply: 4000, + ..Default::default() } )] ); assert_eq!( context.index.get_rune_balances(), - [( - OutPoint { - txid: txid0, - vout: 0 - }, - vec![(id, u128::max_value())] - )] + [ + (OutPoint { txid, vout: 0 }, vec![(id, 1000)]), + (OutPoint { txid, vout: 1 }, vec![(id, 1000)]), + (OutPoint { txid, vout: 2 }, vec![(id, 1000)]), + (OutPoint { txid, vout: 3 }, vec![(id, 1000)]), + ] ); + } - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], - outputs: 2, + #[test] + fn split_in_etching_with_amount_with_preceding_edict() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + outputs: 4, op_return: Some( Runestone { - edicts: vec![Edict { - id: id.into(), - amount: 0, - output: 1, - }], - etching: None, + edicts: vec![ + Edict { + id: 0, + amount: u128::max_value() - 3000, + output: 0, + }, + Edict { + id: 0, + amount: 1000, + output: 5, + }, + ], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() } .encipher(), ), @@ -2690,17 +2873,1019 @@ mod tests { context.mine_blocks(1); + let id = RuneId { + height: 2, + index: 1, + }; + assert_eq!( - context.index.runes().unwrap().unwrap(), + context.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, - etching: txid0, + etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { txid, vout: 0 }, + vec![(id, u128::max_value() - 2000)] + ), + (OutPoint { txid, vout: 1 }, vec![(id, 1000)]), + (OutPoint { txid, vout: 2 }, vec![(id, 1000)]), + ] + ); + } + + #[test] + fn split_in_etching_with_amount_with_following_edict() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + outputs: 4, + op_return: Some( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 1000, + output: 5, + }, + Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }, + ], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { txid, vout: 0 }, + vec![(id, u128::max_value() - 4000 + 1000)] + ), + (OutPoint { txid, vout: 1 }, vec![(id, 1000)]), + (OutPoint { txid, vout: 2 }, vec![(id, 1000)]), + (OutPoint { txid, vout: 3 }, vec![(id, 1000)]), + ] + ); + } + + #[test] + fn split() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + divisibility: 0, + rune: Rune(RUNE), + symbol: None, + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [( + OutPoint { + txid: txid0, + vout: 0 + }, + vec![(id, u128::max_value())] + )] + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + edicts: vec![Edict { + id: id.into(), + amount: 0, + output: 3, + }], + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { + txid: txid1, + vout: 0 + }, + vec![(id, u128::max_value() / 2 + 1)] + ), + ( + OutPoint { + txid: txid1, + vout: 1 + }, + vec![(id, u128::max_value() / 2)] + ) + ] + ); + } + + #[test] + fn split_with_preceding_edict() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [( + OutPoint { + txid: txid0, + vout: 0 + }, + vec![(id, u128::max_value())] + )] + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + edicts: vec![ + Edict { + id: id.into(), + amount: 1000, + output: 0, + }, + Edict { + id: id.into(), + amount: 0, + output: 3, + }, + ], + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { + txid: txid1, + vout: 0 + }, + vec![(id, 1000 + (u128::max_value() - 1000) / 2 + 1)], + ), + ( + OutPoint { + txid: txid1, + vout: 1 + }, + vec![(id, (u128::max_value() - 1000) / 2)], + ) + ] + ); + } + + #[test] + fn split_with_following_edict() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [( + OutPoint { + txid: txid0, + vout: 0 + }, + vec![(id, u128::max_value())] + )] + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + edicts: vec![ + Edict { + id: id.into(), + amount: 0, + output: 3, + }, + Edict { + id: id.into(), + amount: 1000, + output: 1, + }, + ], + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { + txid: txid1, + vout: 0 + }, + vec![(id, u128::max_value() / 2)], + ), + ( + OutPoint { + txid: txid1, + vout: 1 + }, + vec![(id, u128::max_value() / 2 + 1)], + ) + ] + ); + } + + #[test] + fn split_with_amount() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [( + OutPoint { + txid: txid0, + vout: 0 + }, + vec![(id, u128::max_value())] + )] + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + edicts: vec![Edict { + id: id.into(), + amount: 1000, + output: 3, + }], + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { + txid: txid1, + vout: 0 + }, + vec![(id, u128::max_value() - 1000)] + ), + ( + OutPoint { + txid: txid1, + vout: 1 + }, + vec![(id, 1000)] + ) + ] + ); + } + + #[test] + fn split_with_amount_with_preceding_edict() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [( + OutPoint { + txid: txid0, + vout: 0 + }, + vec![(id, u128::max_value())] + )] + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 4, + op_return: Some( + Runestone { + edicts: vec![ + Edict { + id: id.into(), + amount: u128::max_value() - 2000, + output: 0, + }, + Edict { + id: id.into(), + amount: 1000, + output: 5, + }, + ], + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { + txid: txid1, + vout: 0 + }, + vec![(id, u128::max_value() - 2000 + 1000)] + ), + ( + OutPoint { + txid: txid1, + vout: 1 + }, + vec![(id, 1000)] + ) + ] + ); + } + + #[test] + fn split_with_amount_with_following_edict() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [( + OutPoint { + txid: txid0, + vout: 0 + }, + vec![(id, u128::max_value())] + )] + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 4, + op_return: Some( + Runestone { + edicts: vec![ + Edict { + id: id.into(), + amount: 1000, + output: 5, + }, + Edict { + id: id.into(), + amount: u128::max_value(), + output: 0, + }, + ], + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [ + ( + OutPoint { + txid: txid1, + vout: 0 + }, + vec![(id, u128::max_value() - 4000 + 1000)] + ), + ( + OutPoint { + txid: txid1, + vout: 1 + }, + vec![(id, 1000)] + ), + ( + OutPoint { + txid: txid1, + vout: 2 + }, + vec![(id, 1000)] + ), + ( + OutPoint { + txid: txid1, + vout: 3 + }, + vec![(id, 1000)] + ) + ] + ); + } + + #[test] + fn etching_may_specify_symbol() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + symbol: Some('$'), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid, + rune: Rune(RUNE), + supply: u128::max_value(), + symbol: Some('$'), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])] + ); + } + + #[test] + fn allocate_all_remaining_runes_in_etching() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: 0, + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])] + ); + } + + #[test] + fn allocate_all_remaining_runes_in_inputs() { + let context = Context::builder() + .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") + .build(); + + context.mine_blocks(1); + + let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, Witness::new())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune: Rune(RUNE), + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + let id = RuneId { + height: 2, + index: 1, + }; + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() + } + )] + ); + + assert_eq!( + context.index.get_rune_balances(), + [( + OutPoint { + txid: txid0, + vout: 0 + }, + vec![(id, u128::max_value())] + )] + ); + + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 1, 0, Witness::new())], + outputs: 2, + op_return: Some( + Runestone { + edicts: vec![Edict { + id: id.into(), + amount: 0, + output: 1, + }], + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + context.mine_blocks(1); + + assert_eq!( + context.index.runes().unwrap(), + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::max_value(), + ..Default::default() } )] ); diff --git a/src/runes/rune_id.rs b/src/runes/rune_id.rs index dce79f1368..02a57d61ee 100644 --- a/src/runes/rune_id.rs +++ b/src/runes/rune_id.rs @@ -29,6 +29,21 @@ impl Display for RuneId { } } +impl FromStr for RuneId { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + let (height, index) = s + .split_once('/') + .ok_or_else(|| anyhow!("invalid rune ID: {s}"))?; + + Ok(Self { + height: height.parse()?, + index: index.parse()?, + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -57,6 +72,22 @@ mod tests { ); } + #[test] + fn from_str() { + assert!("/".parse::().is_err()); + assert!("1/".parse::().is_err()); + assert!("/2".parse::().is_err()); + assert!("a/2".parse::().is_err()); + assert!("1/a".parse::().is_err()); + assert_eq!( + "1/2".parse::().unwrap(), + RuneId { + height: 1, + index: 2 + } + ); + } + #[test] fn try_from() { assert_eq!( diff --git a/src/runes/runestone.rs b/src/runes/runestone.rs index bb343b0c21..ad8cd17a3f 100644 --- a/src/runes/runestone.rs +++ b/src/runes/runestone.rs @@ -1,9 +1,55 @@ use super::*; +const TAG_BODY: u128 = 0; +const TAG_DIVISIBILITY: u128 = 1; +const TAG_RUNE: u128 = 2; +const TAG_SYMBOL: u128 = 3; + +#[allow(unused)] +const TAG_BURN: u128 = 4; + #[derive(Default, Serialize, Debug, PartialEq)] pub struct Runestone { pub edicts: Vec, pub etching: Option, + pub burn: bool, +} + +struct Message { + fields: HashMap, + body: Vec, +} + +impl Message { + fn from_integers(payload: &[u128]) -> Self { + let mut body = Vec::new(); + let mut fields = HashMap::new(); + + for i in (0..payload.len()).step_by(2) { + let tag = payload[i]; + + if tag == TAG_BODY { + let mut id = 0u128; + for chunk in payload[i + 1..].chunks_exact(3) { + id = id.saturating_add(chunk[0]); + body.push(Edict { + id, + amount: chunk[1], + output: chunk[2], + }); + } + break; + } + + let Some(&value) = payload.get(i + 1) else { + break; + }; + + fields.entry(tag).or_insert(value); + } + + Self { fields, body } + } } impl Runestone { @@ -25,68 +71,64 @@ impl Runestone { i += length; } - let mut edicts = Vec::new(); - let mut etching = None; - let mut id = 0u128; - for chunk in integers.chunks(3) { - match *chunk { - [id_delta, amount, output] => { - id = id.saturating_add(id_delta); - edicts.push(Edict { id, amount, output }); - } - [rune] => { - etching = Some(Etching { - divisibility: 0, - rune: Rune(rune), - symbol: None, - }) - } - [rune, parameters] => { - etching = Some(Etching { - divisibility: u8::try_from(parameters & 0b11_1111) - .unwrap() - .min(MAX_DIVISIBILITY), - rune: Rune(rune), - symbol: { - let symbol = u32::try_from(parameters >> 6 & 0xFFFFFFFF).unwrap(); - if symbol > 0 { - char::from_u32(symbol) - } else { - None - } - }, - }) - } - _ => unreachable!(), - } - } + let Message { mut fields, body } = Message::from_integers(&integers); + + let etching = fields.remove(&TAG_RUNE).map(|rune| Etching { + rune: Rune(rune), + divisibility: fields + .remove(&TAG_DIVISIBILITY) + .and_then(|divisibility| u8::try_from(divisibility).ok()) + .and_then(|divisibility| (divisibility <= MAX_DIVISIBILITY).then_some(divisibility)) + .unwrap_or_default(), + symbol: fields + .remove(&TAG_SYMBOL) + .and_then(|symbol| u32::try_from(symbol).ok()) + .and_then(char::from_u32), + }); - Ok(Some(Self { edicts, etching })) + Ok(Some(Self { + edicts: body, + etching, + burn: fields.keys().any(|tag| tag % 2 == 0), + })) } #[cfg(test)] pub(crate) fn encipher(&self) -> ScriptBuf { let mut payload = Vec::new(); - let mut edicts = self.edicts.clone(); - edicts.sort_by_key(|edict| edict.id); + if let Some(etching) = self.etching { + varint::encode_to_vec(TAG_RUNE, &mut payload); + varint::encode_to_vec(etching.rune.0, &mut payload); + + if etching.divisibility != 0 && etching.divisibility <= MAX_DIVISIBILITY { + varint::encode_to_vec(TAG_DIVISIBILITY, &mut payload); + varint::encode_to_vec(etching.divisibility.into(), &mut payload); + } - let mut id = 0; - for edict in edicts { - varint::encode_to_vec(edict.id - id, &mut payload); - varint::encode_to_vec(edict.amount, &mut payload); - varint::encode_to_vec(edict.output, &mut payload); - id = edict.id; + if let Some(symbol) = etching.symbol { + varint::encode_to_vec(TAG_SYMBOL, &mut payload); + varint::encode_to_vec(symbol.into(), &mut payload); + } } - if let Some(etching) = self.etching { - varint::encode_to_vec(etching.rune.0, &mut payload); + if self.burn { + varint::encode_to_vec(TAG_BURN, &mut payload); + varint::encode_to_vec(0, &mut payload); + } + + if !self.edicts.is_empty() { + varint::encode_to_vec(TAG_BODY, &mut payload); - let parameters = - u128::from(etching.symbol.unwrap_or_default()) << 6 | u128::from(etching.divisibility); + let mut edicts = self.edicts.clone(); + edicts.sort_by_key(|edict| edict.id); - if parameters != 0 { - varint::encode_to_vec(parameters, &mut payload); + let mut id = 0; + for edict in edicts { + varint::encode_to_vec(edict.id - id, &mut payload); + varint::encode_to_vec(edict.amount, &mut payload); + varint::encode_to_vec(edict.output, &mut payload); + id = edict.id; } } @@ -326,10 +368,7 @@ mod tests { lock_time: locktime::absolute::LockTime::ZERO, version: 0, }), - Ok(Some(Runestone { - edicts: Vec::new(), - etching: None, - })) + Ok(Some(Runestone::default())) ); } @@ -345,7 +384,7 @@ mod tests { #[test] fn error_in_input_aborts_search_for_runestone() { - let payload = payload(&[1, 2, 3]); + let payload = payload(&[0, 1, 2, 3]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -382,7 +421,73 @@ mod tests { #[test] fn deciphering_non_empty_runestone_is_successful() { - let payload = payload(&[1, 2, 3]); + let payload = payload(&[0, 1, 2, 3]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"RUNE_TEST") + .push_slice(payload) + .into_script(), + value: 0 + }], + lock_time: locktime::absolute::LockTime::ZERO, + version: 0, + }), + Ok(Some(Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, + }], + ..Default::default() + })) + ); + } + + #[test] + fn decipher_etching() { + let payload = payload(&[2, 4, 0, 1, 2, 3]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"RUNE_TEST") + .push_slice(payload) + .into_script(), + value: 0 + }], + lock_time: locktime::absolute::LockTime::ZERO, + version: 0, + }), + Ok(Some(Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, + }], + etching: Some(Etching { + rune: Rune(4), + ..Default::default() + }), + ..Default::default() + })) + ); + } + + #[test] + fn duplicate_tags_are_ignored() { + let payload = payload(&[2, 4, 2, 5, 0, 1, 2, 3]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -406,14 +511,79 @@ mod tests { amount: 2, output: 3, }], - etching: None, + etching: Some(Etching { + rune: Rune(4), + ..Default::default() + }), + ..Default::default() + })) + ); + } + + #[test] + fn unrecognized_odd_tag_is_ignored() { + let payload = payload(&[127, 100, 0, 1, 2, 3]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"RUNE_TEST") + .push_slice(payload) + .into_script(), + value: 0 + }], + lock_time: locktime::absolute::LockTime::ZERO, + version: 0, + }), + Ok(Some(Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, + }], + ..Default::default() + })) + ); + } + + #[test] + fn tag_with_no_value_is_ignored() { + let payload = payload(&[2, 4, 2]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"RUNE_TEST") + .push_slice(payload) + .into_script(), + value: 0 + }], + lock_time: locktime::absolute::LockTime::ZERO, + version: 0, + }), + Ok(Some(Runestone { + etching: Some(Etching { + rune: Rune(4), + ..Default::default() + }), + ..Default::default() })) ); } #[test] - fn additional_integer_is_rune() { - let payload = payload(&[1, 2, 3, 4]); + fn additional_integers_in_body_are_ignored() { + let payload = payload(&[2, 4, 0, 1, 2, 3, 4, 5]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -439,16 +609,16 @@ mod tests { }], etching: Some(Etching { rune: Rune(4), - divisibility: 0, - symbol: None, + ..Default::default() }), + ..Default::default() })) ); } #[test] - fn additional_two_integers_are_rune_and_divisibility() { - let payload = payload(&[1, 2, 3, 4, 5]); + fn decipher_etching_with_divisibility() { + let payload = payload(&[2, 4, 1, 5, 0, 1, 2, 3]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -475,15 +645,16 @@ mod tests { etching: Some(Etching { rune: Rune(4), divisibility: 5, - symbol: None, + ..Default::default() }), + ..Default::default() })) ); } #[test] - fn divisibility_above_max_is_clamped() { - let payload = payload(&[1, 2, 3, 4, (MAX_DIVISIBILITY + 1).into()]); + fn divisibility_above_max_is_ignored() { + let payload = payload(&[2, 4, 1, (MAX_DIVISIBILITY + 1).into(), 0, 1, 2, 3]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -509,16 +680,16 @@ mod tests { }], etching: Some(Etching { rune: Rune(4), - divisibility: MAX_DIVISIBILITY, - symbol: None, + ..Default::default() }), + ..Default::default() })) ); } #[test] - fn divisibility_is_taken_from_bits_five_to_zero() { - let payload = payload(&[1, 2, 3, 4, 0b110_0000]); + fn symbol_above_max_is_ignored() { + let payload = payload(&[2, 4, 3, u128::from(u32::from(char::MAX) + 1), 0, 1, 2, 3]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -544,16 +715,16 @@ mod tests { }], etching: Some(Etching { rune: Rune(4), - divisibility: 0b10_0000, - symbol: Some(1.into()), + ..Default::default() }), + ..Default::default() })) ); } #[test] - fn symbol_is_taken_from_bits_thirty_seven_to_six() { - let payload = payload(&[1, 2, 3, 4, u128::from('a') << 6]); + fn decipher_etching_with_symbol() { + let payload = payload(&[2, 4, 3, 'a'.into(), 0, 1, 2, 3]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -579,16 +750,88 @@ mod tests { }], etching: Some(Etching { rune: Rune(4), - divisibility: 0, symbol: Some('a'), + ..Default::default() + }), + ..Default::default() + })) + ); + } + + #[test] + fn decipher_etching_with_divisibility_and_symbol() { + let payload = payload(&[2, 4, 1, 1, 3, 'a'.into(), 0, 1, 2, 3]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"RUNE_TEST") + .push_slice(payload) + .into_script(), + value: 0 + }], + lock_time: locktime::absolute::LockTime::ZERO, + version: 0, + }), + Ok(Some(Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, + }], + etching: Some(Etching { + rune: Rune(4), + divisibility: 1, + symbol: Some('a'), + }), + ..Default::default() + })) + ); + } + + #[test] + fn tag_values_are_not_parsed_as_tags() { + let payload = payload(&[2, 4, 1, 0, 0, 1, 2, 3]); + + let payload: &PushBytes = payload.as_slice().try_into().unwrap(); + + assert_eq!( + Runestone::decipher(&Transaction { + input: Vec::new(), + output: vec![TxOut { + script_pubkey: script::Builder::new() + .push_opcode(opcodes::all::OP_RETURN) + .push_slice(b"RUNE_TEST") + .push_slice(payload) + .into_script(), + value: 0 + }], + lock_time: locktime::absolute::LockTime::ZERO, + version: 0, + }), + Ok(Some(Runestone { + edicts: vec![Edict { + id: 1, + amount: 2, + output: 3, + }], + etching: Some(Etching { + rune: Rune(4), + ..Default::default() }), + ..Default::default() })) ); } #[test] fn runestone_may_contain_multiple_edicts() { - let payload = payload(&[1, 2, 3, 3, 5, 6]); + let payload = payload(&[0, 1, 2, 3, 3, 5, 6]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -619,14 +862,14 @@ mod tests { output: 6, }, ], - etching: None, + ..Default::default() })) ); } #[test] fn id_deltas_saturate_to_max() { - let payload = payload(&[1, 2, 3, u128::max_value(), 5, 6]); + let payload = payload(&[0, 1, 2, 3, u128::max_value(), 5, 6]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -657,7 +900,7 @@ mod tests { output: 6, }, ], - etching: None, + ..Default::default() })) ); } @@ -671,11 +914,14 @@ mod tests { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) .push_slice(b"RUNE_TEST") - .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(3).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(4).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) .push_slice::<&PushBytes>(varint::encode(5).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(0).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) + .push_slice::<&PushBytes>(varint::encode(3).as_slice().try_into().unwrap()) .into_script(), value: 0 }], @@ -691,15 +937,16 @@ mod tests { etching: Some(Etching { rune: Rune(4), divisibility: 5, - symbol: None, - }) + ..Default::default() + }), + ..Default::default() })) ); } #[test] fn runestone_may_be_in_second_output() { - let payload = payload(&[1, 2, 3]); + let payload = payload(&[0, 1, 2, 3]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -728,15 +975,15 @@ mod tests { id: 1, amount: 2, output: 3, - },], - etching: None, + }], + ..Default::default() })) ); } #[test] fn runestone_may_be_after_non_matching_op_return() { - let payload = payload(&[1, 2, 3]); + let payload = payload(&[0, 1, 2, 3]); let payload: &PushBytes = payload.as_slice().try_into().unwrap(); @@ -768,8 +1015,8 @@ mod tests { id: 1, amount: 2, output: 3, - },], - etching: None, + }], + ..Default::default() })) ); } @@ -779,7 +1026,15 @@ mod tests { #[track_caller] fn case(edicts: Vec, etching: Option, size: usize) { assert_eq!( - Runestone { edicts, etching }.encipher().len() - 1 - b"RUNE_TEST".len(), + Runestone { + edicts, + etching, + ..Default::default() + } + .encipher() + .len() + - 1 + - b"RUNE_TEST".len(), size ); } @@ -789,11 +1044,10 @@ mod tests { case( Vec::new(), Some(Etching { - divisibility: 0, rune: Rune(0), - symbol: None, + ..Default::default() }), - 3, + 4, ); case( @@ -801,9 +1055,9 @@ mod tests { Some(Etching { divisibility: MAX_DIVISIBILITY, rune: Rune(0), - symbol: None, + ..Default::default() }), - 4, + 6, ); case( @@ -813,17 +1067,16 @@ mod tests { rune: Rune(0), symbol: Some('$'), }), - 5, + 8, ); case( Vec::new(), Some(Etching { - divisibility: 0, rune: Rune(u128::max_value()), - symbol: None, + ..Default::default() }), - 21, + 22, ); case( @@ -839,9 +1092,9 @@ mod tests { Some(Etching { divisibility: MAX_DIVISIBILITY, rune: Rune(u128::max_value()), - symbol: None, + ..Default::default() }), - 25, + 28, ); case( @@ -857,9 +1110,33 @@ mod tests { Some(Etching { divisibility: MAX_DIVISIBILITY, rune: Rune(u128::max_value()), - symbol: None, + ..Default::default() }), - 43, + 46, + ); + + case( + vec![Edict { + amount: 0, + id: RuneId { + height: 1_000_000, + index: u16::max_value(), + } + .into(), + output: 0, + }], + None, + 11, + ); + + case( + vec![Edict { + amount: 0, + id: 1 << 48, + output: 0, + }], + None, + 12, ); case( @@ -873,7 +1150,7 @@ mod tests { output: 0, }], None, - 28, + 29, ); case( @@ -898,7 +1175,7 @@ mod tests { }, ], None, - 49, + 50, ); case( @@ -932,7 +1209,7 @@ mod tests { }, ], None, - 70, + 71, ); case( @@ -949,7 +1226,7 @@ mod tests { 4 ], None, - 55, + 56, ); case( @@ -966,7 +1243,7 @@ mod tests { 5 ], None, - 67, + 68, ); case( @@ -983,7 +1260,7 @@ mod tests { 5 ], None, - 64, + 65, ); case( @@ -1000,7 +1277,7 @@ mod tests { 5 ], None, - 62, + 63, ); } } diff --git a/src/subcommand.rs b/src/subcommand.rs index a7a1159415..ee005aaf94 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -20,8 +20,6 @@ pub(crate) enum Subcommand { Decode(decode::Decode), #[command(about = "List the first satoshis of each reward epoch")] Epochs, - #[command(about = "Run an explorer server populated with inscriptions")] - Preview(preview::Preview), #[command(about = "Find a satoshi's current location")] Find(find::Find), #[command(subcommand, about = "Index commands")] @@ -32,10 +30,12 @@ pub(crate) enum Subcommand { List(list::List), #[command(about = "Parse a satoshi from ordinal notation")] Parse(parse::Parse), - #[command(about = "Display information about a block's subsidy")] - Subsidy(subsidy::Subsidy), + #[command(about = "Run an explorer server populated with inscriptions")] + Preview(preview::Preview), #[command(about = "Run the explorer server")] Server(server::Server), + #[command(about = "Display information about a block's subsidy")] + Subsidy(subsidy::Subsidy), #[command(about = "Display Bitcoin supply information")] Supply, #[command(about = "Display satoshi traits")] @@ -49,19 +49,19 @@ impl Subcommand { match self { Self::Decode(decode) => decode.run(), Self::Epochs => epochs::run(), - Self::Preview(preview) => preview.run(), Self::Find(find) => find.run(options), Self::Index(index) => index.run(options), Self::Info(info) => info.run(options), Self::List(list) => list.run(options), Self::Parse(parse) => parse.run(), - Self::Subsidy(subsidy) => subsidy.run(), + Self::Preview(preview) => preview.run(), Self::Server(server) => { let index = Arc::new(Index::open(&options)?); let handle = axum_server::Handle::new(); LISTENERS.lock().unwrap().push(handle.clone()); server.run(options, index, handle) } + Self::Subsidy(subsidy) => subsidy.run(), Self::Supply => supply::run(), Self::Traits(traits) => traits.run(), Self::Wallet(wallet) => wallet.run(options), diff --git a/src/subcommand/find.rs b/src/subcommand/find.rs index b52601a2c6..deb28c44f2 100644 --- a/src/subcommand/find.rs +++ b/src/subcommand/find.rs @@ -24,6 +24,10 @@ impl Find { pub(crate) fn run(self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; + if !index.has_sat_index() { + bail!("find requires index created with `--index-sats` flag"); + } + index.update()?; match self.end { diff --git a/src/subcommand/list.rs b/src/subcommand/list.rs index e995522808..c0477407c0 100644 --- a/src/subcommand/list.rs +++ b/src/subcommand/list.rs @@ -21,6 +21,10 @@ impl List { pub(crate) fn run(self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; + if !index.has_sat_index() { + bail!("list requires index created with `--index-sats` flag"); + } + index.update()?; match index.list(self.outpoint)? { diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 96abe5b89f..fcd107a832 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -178,6 +178,7 @@ impl Server { let page_config = Arc::new(PageConfig { chain: options.chain(), domain: acme_domains.first().cloned(), + index_sats: index.has_sat_index(), }); let router = Router::new() @@ -217,7 +218,7 @@ impl Server { .route("/runes", get(Self::runes)) .route("/sat/:sat", get(Self::sat)) .route("/search", get(Self::search_by_query)) - .route("/search/:query", get(Self::search_by_path)) + .route("/search/*query", get(Self::search_by_path)) .route("/static/*path", get(Self::static_asset)) .route("/status", get(Self::status)) .route("/tx/:txid", get(Self::transaction)) @@ -465,7 +466,7 @@ impl Server { blocktime, inscriptions, } - .page(page_config, index.has_sat_index()?) + .page(page_config) .into_response() }) } @@ -480,11 +481,7 @@ impl Server { Path(outpoint): Path, accept_json: AcceptJson, ) -> ServerResult { - let list = if index.has_sat_index()? { - index.list(outpoint)? - } else { - None - }; + let list = index.list(outpoint)?; let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { let mut value = 0; @@ -535,14 +532,13 @@ impl Server { output, runes, } - .page(page_config, index.has_sat_index()?) + .page(page_config) .into_response() }) } async fn range( Extension(page_config): Extension>, - Extension(index): Extension>, Path((DeserializeFromStr(start), DeserializeFromStr(end))): Path<( DeserializeFromStr, DeserializeFromStr, @@ -553,16 +549,12 @@ impl Server { Ordering::Greater => Err(ServerError::BadRequest( "range start greater than range end".to_string(), )), - Ordering::Less => Ok(RangeHtml { start, end }.page(page_config, index.has_sat_index()?)), + Ordering::Less => Ok(RangeHtml { start, end }.page(page_config)), } } async fn rare_txt(Extension(index): Extension>) -> ServerResult { - Ok(RareTxt(index.rare_sat_satpoints()?.ok_or_else(|| { - ServerError::NotFound( - "tracking rare sats requires index created with `--index-sats` flag".into(), - ) - })?)) + Ok(RareTxt(index.rare_sat_satpoints()?)) } async fn rune( @@ -591,7 +583,7 @@ impl Server { entry, inscription, } - .page(page_config, index.has_sat_index()?), + .page(page_config), ) } @@ -599,13 +591,12 @@ impl Server { Extension(page_config): Extension>, Extension(index): Extension>, ) -> ServerResult> { - let entries = index.runes()?.ok_or_else(|| { - ServerError::NotFound( - "tracking runes requires index created with `--index-runes-pre-alpha-i-agree-to-get-rekt` flag".into(), - ) - })?; - - Ok(RunesHtml { entries }.page(page_config, index.has_sat_index()?)) + Ok( + RunesHtml { + entries: index.runes()?, + } + .page(page_config), + ) } async fn home( @@ -621,7 +612,7 @@ impl Server { featured_blocks.insert(*hash, inscriptions); } - Ok(HomeHtml::new(blocks, featured_blocks).page(page_config, index.has_sat_index()?)) + Ok(HomeHtml::new(blocks, featured_blocks).page(page_config)) } async fn install_script() -> Redirect { @@ -665,7 +656,7 @@ impl Server { total_num, featured_inscriptions, ) - .page(page_config, index.has_sat_index()?), + .page(page_config), ) } @@ -687,7 +678,7 @@ impl Server { inscription.map(|_| InscriptionId { txid, index: 0 }), page_config.chain, ) - .page(page_config, index.has_sat_index()?), + .page(page_config), ) } @@ -726,8 +717,10 @@ impl Server { fn search_inner(index: &Index, query: &str) -> ServerResult { lazy_static! { static ref HASH: Regex = Regex::new(r"^[[:xdigit:]]{64}$").unwrap(); - static ref OUTPOINT: Regex = Regex::new(r"^[[:xdigit:]]{64}:\d+$").unwrap(); static ref INSCRIPTION_ID: Regex = Regex::new(r"^[[:xdigit:]]{64}i\d+$").unwrap(); + static ref OUTPOINT: Regex = Regex::new(r"^[[:xdigit:]]{64}:\d+$").unwrap(); + static ref RUNE: Regex = Regex::new(r"^[A-Z]+$").unwrap(); + static ref RUNE_ID: Regex = Regex::new(r"^[0-9]+/[0-9]+$").unwrap(); } let query = query.trim(); @@ -742,6 +735,16 @@ impl Server { Ok(Redirect::to(&format!("/output/{query}"))) } else if INSCRIPTION_ID.is_match(query) { Ok(Redirect::to(&format!("/inscription/{query}"))) + } else if RUNE.is_match(query) { + Ok(Redirect::to(&format!("/rune/{query}"))) + } else if RUNE_ID.is_match(query) { + let id = query + .parse::() + .map_err(|err| ServerError::BadRequest(err.to_string()))?; + + let rune = index.get_rune_by_id(id)?.ok_or_not_found(|| "rune ID")?; + + Ok(Redirect::to(&format!("/rune/{rune}"))) } else { Ok(Redirect::to(&format!("/sat/{query}"))) } @@ -900,7 +903,7 @@ impl Server { .nth(path.2) .ok_or_not_found(not_found)?; - Ok(InputHtml { path, input }.page(page_config, index.has_sat_index()?)) + Ok(InputHtml { path, input }.page(page_config)) } async fn faq() -> Redirect { @@ -1128,7 +1131,7 @@ impl Server { satpoint, timestamp: timestamp(entry.timestamp), } - .page(page_config, index.has_sat_index()?) + .page(page_config) .into_response() }) } @@ -1173,7 +1176,7 @@ impl Server { inscriptions, page_index, )? - .page(page_config, index.has_sat_index()?) + .page(page_config) .into_response() }) } @@ -1220,7 +1223,7 @@ impl Server { next, prev, } - .page(page_config, index.has_sat_index()?) + .page(page_config) .into_response() }) } @@ -1704,6 +1707,11 @@ mod tests { TestServer::new().assert_redirect("/search?query=0", "/sat/0"); } + #[test] + fn search_by_query_returns_rune() { + TestServer::new().assert_redirect("/search?query=ABCD", "/rune/ABCD"); + } + #[test] fn search_by_query_returns_inscription() { TestServer::new().assert_redirect( @@ -1754,6 +1762,55 @@ mod tests { ); } + #[test] + fn search_by_path_returns_rune() { + TestServer::new().assert_redirect("/search/ABCD", "/rune/ABCD"); + } + + #[test] + fn search_by_rune_id_returns_rune() { + let server = TestServer::new_with_regtest_with_index_runes(); + + server.mine_blocks(1); + + let rune = Rune(RUNE); + + server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); + + server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], + op_return: Some( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::max_value(), + output: 0, + }], + etching: Some(Etching { + rune, + ..Default::default() + }), + ..Default::default() + } + .encipher(), + ), + ..Default::default() + }); + + server.mine_blocks(1); + + server.assert_redirect("/search/2/1", "/rune/AAAAAAAAAAAAA"); + server.assert_redirect("/search?query=2/1", "/rune/AAAAAAAAAAAAA"); + + server.assert_response_regex("/rune/100/200", StatusCode::NOT_FOUND, ".*"); + + server.assert_response_regex( + "/search/100000000000000000000/200000000000000000", + StatusCode::BAD_REQUEST, + ".*", + ); + } + #[test] fn http_to_https_redirect_with_path() { TestServer::new_with_args(&[], &["--redirect-http-to-https", "--https"]).assert_redirect( @@ -2069,7 +2126,7 @@ mod tests {
id
{inscription_id}
preview
.*
output
-
0000000000000000000000000000000000000000000000000000000000000000:0 \\(unbound\\)
.*" +
0000000000000000000000000000000000000000000000000000000000000000:0
.*" ), ); } @@ -2266,7 +2323,7 @@ mod tests { } #[test] - fn rare_with_index() { + fn rare_with_sat_index() { TestServer::new_with_sat_index().assert_response( "/rare.txt", StatusCode::OK, @@ -2280,8 +2337,9 @@ mod tests { fn rare_without_sat_index() { TestServer::new().assert_response( "/rare.txt", - StatusCode::NOT_FOUND, - "tracking rare sats requires index created with `--index-sats` flag", + StatusCode::OK, + "sat\tsatpoint +", ); } @@ -3151,10 +3209,10 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune: Rune(RUNE), - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -3169,16 +3227,14 @@ mod tests { }; assert_eq!( - server.index.runes().unwrap().unwrap(), + server.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune: Rune(RUNE), supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); @@ -3219,10 +3275,11 @@ mod tests { output: 0, }], etching: Some(Etching { - divisibility: 0, rune, symbol: Some('$'), + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -3237,16 +3294,15 @@ mod tests { }; assert_eq!( - server.index.runes().unwrap().unwrap(), + server.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, - divisibility: 0, etching: txid, rune, supply: u128::max_value(), symbol: Some('$'), + ..Default::default() } )] ); @@ -3327,7 +3383,7 @@ mod tests { "/inscription/-1", StatusCode::OK, format!( - ".*

Inscription -1 \\(unstable\\)

.* + ".*

Inscription -1

.*
id
{cursed_inscription_id}
.*" @@ -3357,8 +3413,9 @@ mod tests { etching: Some(Etching { divisibility: 1, rune, - symbol: None, + ..Default::default() }), + ..Default::default() } .encipher(), ), @@ -3373,16 +3430,15 @@ mod tests { }; assert_eq!( - server.index.runes().unwrap().unwrap(), + server.index.runes().unwrap(), [( id, RuneEntry { - burned: 0, divisibility: 1, etching: txid, rune, supply: u128::max_value(), - symbol: None, + ..Default::default() } )] ); diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index 18155b69eb..8b2b8fc23c 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -26,6 +26,11 @@ pub struct OutputRare { impl Sats { pub(crate) fn run(&self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; + + if !index.has_sat_index() { + bail!("sats requires index created with `--index-sats` flag"); + } + index.update()?; let utxos = index.get_unspent_output_ranges(Wallet::load(&options)?)?; diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 4e7c549640..bac9934bdf 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -121,7 +121,7 @@ impl TransactionBuilder { pub(crate) const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); pub(crate) const MAX_POSTAGE: Amount = Amount::from_sat(2 * 10_000); - pub(crate) fn new( + pub fn new( outgoing: SatPoint, inscriptions: BTreeMap, amounts: BTreeMap, @@ -145,7 +145,7 @@ impl TransactionBuilder { } } - pub(crate) fn build_transaction(self) -> Result { + pub fn build_transaction(self) -> Result { if self.change_addresses.len() < 2 { return Err(Error::DuplicateAddress( self.change_addresses.first().unwrap().clone(), diff --git a/src/templates.rs b/src/templates.rs index 993a884b6f..5b5f456136 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -45,24 +45,19 @@ mod transaction; #[derive(Boilerplate)] pub(crate) struct PageHtml { content: T, - has_sat_index: bool, - page_config: Arc, + config: Arc, } impl PageHtml where T: PageContent, { - pub(crate) fn new(content: T, page_config: Arc, has_sat_index: bool) -> Self { - Self { - content, - has_sat_index, - page_config, - } + pub(crate) fn new(content: T, config: Arc) -> Self { + Self { content, config } } fn og_image(&self) -> String { - if let Some(domain) = &self.page_config.domain { + if let Some(domain) = &self.config.domain { format!("https://{domain}/static/favicon.png") } else { "https://ordinals.com/static/favicon.png".into() @@ -70,10 +65,10 @@ where } fn superscript(&self) -> String { - if self.page_config.chain == Chain::Mainnet { + if self.config.chain == Chain::Mainnet { "alpha".into() } else { - self.page_config.chain.to_string() + self.config.chain.to_string() } } } @@ -81,11 +76,11 @@ where pub(crate) trait PageContent: Display + 'static { fn title(&self) -> String; - fn page(self, page_config: Arc, has_sat_index: bool) -> PageHtml + fn page(self, page_config: Arc) -> PageHtml where Self: Sized, { - PageHtml::new(self, page_config, has_sat_index) + PageHtml::new(self, page_config) } fn preview_image_url(&self) -> Option> { @@ -114,13 +109,11 @@ mod tests { #[test] fn page() { assert_regex_match!( - Foo.page( - Arc::new(PageConfig { - chain: Chain::Mainnet, - domain: Some("signet.ordinals.com".into()) - }), - true - ), + Foo.page(Arc::new(PageConfig { + chain: Chain::Mainnet, + domain: Some("signet.ordinals.com".into()), + index_sats: true, + }),), r" @@ -161,13 +154,11 @@ mod tests { #[test] fn page_mainnet() { assert_regex_match!( - Foo.page( - Arc::new(PageConfig { - chain: Chain::Mainnet, - domain: None - }), - true - ), + Foo.page(Arc::new(PageConfig { + chain: Chain::Mainnet, + domain: None, + index_sats: true, + }),), r".*