diff --git a/CHANGELOG.md b/CHANGELOG.md index 382b56690b..6df9466797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ Changelog ========= +[0.12.3](https://github.com/ordinals/ord/releases/tag/0.12.3) - 2023-12-01 +-------------------------------------------------------------------------- + +### Added +- Add `ord balances` to show rune balances (#2782) + +### Fixed +- Fix preview test (#2795) +- Fix reinscriptions charm (#2793) +- Fix fee calculation for batch inscribe on same sat (#2785) + +### Misc +- Add `audit-cache` binary to audit Cloudflare caching (#2787) +- Fix typos (#2791) +- Add total bytes and proportion to database info (#2783) + [0.12.2](https://github.com/ordinals/ord/releases/tag/0.12.2) - 2023-11-29 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index a0d31d9081..6c255b76a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,14 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "audit-cache" +version = "0.0.0" +dependencies = [ + "colored", + "reqwest", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -722,6 +730,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "concurrent-queue" version = "2.3.0" @@ -752,9 +771,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -762,9 +781,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" @@ -1645,7 +1664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.25", + "rustix 0.38.26", "windows-sys 0.48.0", ] @@ -1776,9 +1795,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" @@ -2093,7 +2112,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord-litecoin" -version = "0.12.2" +version = "0.12.3" dependencies = [ "anyhow", "async-trait", @@ -2621,15 +2640,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys 0.4.11", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.12", + "windows-sys 0.52.0", ] [[package]] @@ -3048,7 +3067,7 @@ dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.1", "redox_syscall 0.4.1", - "rustix 0.38.25", + "rustix 0.38.26", "windows-sys 0.48.0", ] diff --git a/Cargo.toml b/Cargo.toml index 63e1c600e0..abe45cfea4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord-litecoin" description = "◉ Ordinal wallet and block explorer for litecoin" -version = "0.12.2" +version = "0.12.3" license = "CC0-1.0" edition = "2021" autotests = false @@ -15,7 +15,7 @@ copyright = "The Ord Maintainers" maintainer = "The Ord Maintainers" [workspace] -members = [".", "test-bitcoincore-rpc"] +members = [".", "test-bitcoincore-rpc", "crates/*"] [dependencies] anyhow = { version = "1.0.56", features = ["backtrace"] } diff --git a/crates/audit-cache/Cargo.toml b/crates/audit-cache/Cargo.toml new file mode 100644 index 0000000000..4cde90b3e8 --- /dev/null +++ b/crates/audit-cache/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "audit-cache" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +colored = "2.0.4" +reqwest = { version = "0.11.22", features = ["blocking"] } diff --git a/crates/audit-cache/src/main.rs b/crates/audit-cache/src/main.rs new file mode 100644 index 0000000000..a1c870f7ce --- /dev/null +++ b/crates/audit-cache/src/main.rs @@ -0,0 +1,88 @@ +use { + colored::Colorize, + reqwest::{blocking::get, StatusCode}, + std::process, +}; + +const ENDPOINTS: &[(&str, StatusCode, &str)] = &[ + // PNG content is cached + ( + "/content/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0", + StatusCode::OK, + "HIT", + ), + // HTML content is cached + ( + "/content/114c5c87c4d0a7facb2b4bf515a4ad385182c076a5cfcc2982bf2df103ec0fffi0", + StatusCode::OK, + "HIT", + ), + // content respopnses that aren't found aren't cached + ( + "/content/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i1", + StatusCode::NOT_FOUND, + "BYPASS", + ), + // HTML previews are cached + ( + "/preview/114c5c87c4d0a7facb2b4bf515a4ad385182c076a5cfcc2982bf2df103ec0fffi0", + StatusCode::OK, + "HIT", + ), + // non-HTML previews are not cached + ( + "/preview/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0", + StatusCode::OK, + "BYPASS", + ), + ("/static/index.css", StatusCode::OK, "HIT"), + ("/static/index.js", StatusCode::OK, "HIT"), + ("/sat/FOO", StatusCode::BAD_REQUEST, "HIT"), + ("/", StatusCode::OK, "BYPASS"), + ("/blockheight", StatusCode::OK, "BYPASS"), +]; + +fn main() { + eprint!("Warming up the cache"); + + for (endpoint, expected_status_code, _expected_cache_status) in ENDPOINTS { + let response = get(format!("https://ordinals.com{endpoint}")).unwrap(); + + assert_eq!(response.status(), *expected_status_code); + + eprint!("."); + } + + eprintln!(); + + let mut failures = 0; + + for (endpoint, expected_status_code, expected_cache_status) in ENDPOINTS { + eprint!("GET {endpoint}"); + + let response = get(format!("https://ordinals.com{endpoint}")).unwrap(); + + let status_code = response.status(); + + eprint!(" {}", status_code.as_u16()); + + assert_eq!(response.status(), *expected_status_code); + + let cache_status = response.headers().get("cf-cache-status").unwrap(); + + let pass = cache_status == expected_cache_status; + + if pass { + eprintln!(" {}", cache_status.to_str().unwrap().green()); + } else { + eprintln!(" {}", cache_status.to_str().unwrap().red()); + } + + failures += u32::from(!pass); + } + + if failures > 0 { + eprintln!("{failures} failures"); + process::exit(1); + } +} diff --git a/docs/src/bounty/3.md b/docs/src/bounty/3.md index 39c916ae19..853eb37640 100644 --- a/docs/src/bounty/3.md +++ b/docs/src/bounty/3.md @@ -12,7 +12,7 @@ sat 0, the first sat to be mined is `nvtdijuwxlp` and the name of sat 2,099,999,997,689,999, the last sat to be mined, is `a`. The bounty is open for submissions until block 840000—the first block after the -fourth halvening. Submissions included in block 840000 or later will not be +fourth halving. Submissions included in block 840000 or later will not be considered. Both parts use [frequency.tsv](frequency.tsv), a list of words and the number diff --git a/docs/src/guides/explorer.md b/docs/src/guides/explorer.md index 4b45873f5d..1f120646e4 100644 --- a/docs/src/guides/explorer.md +++ b/docs/src/guides/explorer.md @@ -1,7 +1,7 @@ Ordinal Explorer ================ -The `ord` binary includes a block explorer. We host a instance of the block +The `ord` binary includes a block explorer. We host an instance of the block explorer on mainnet at [ordinals.com](https://ordinals.com), and on signet at [signet.ordinals.com](https://signet.ordinals.com). @@ -43,7 +43,7 @@ transaction: ### Outputs -Transaction outputs can searched by outpoint, for example, the only output of +Transaction outputs can be searched by outpoint, for example, the only output of the genesis block coinbase transaction: [4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0](https://ordinals.com/search/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0) @@ -78,7 +78,7 @@ JSON-API You can run `ord server` with the `--enable-json-api` flag to access endpoints that return JSON instead of HTML if you set the HTTP `Accept: application/json` -header. The structure of theses objects closely follows +header. The structure of these objects closely follows what is shown in the HTML. These endpoints are: - `/inscription/` diff --git a/docs/src/guides/inscriptions.md b/docs/src/guides/inscriptions.md index 4219082501..8692bed9fd 100644 --- a/docs/src/guides/inscriptions.md +++ b/docs/src/guides/inscriptions.md @@ -309,7 +309,7 @@ See the pending transaction with: ord wallet transactions ``` -Once the send transaction confirms, you can can confirm receipt by running: +Once the send transaction confirms, you can confirm receipt by running: ``` ord wallet inscriptions diff --git a/justfile b/justfile index b8c4e6ec4b..4e549d1f7c 100644 --- a/justfile +++ b/justfile @@ -219,3 +219,6 @@ convert-logo-to-favicon: update-mdbook-theme: curl https://raw.githubusercontent.com/rust-lang/mdBook/v0.4.35/src/theme/index.hbs > docs/theme/index.hbs + +audit-cache: + cargo run --package audit-cache diff --git a/src/index.rs b/src/index.rs index e8e1a14106..55ec600b0a 100644 --- a/src/index.rs +++ b/src/index.rs @@ -122,6 +122,7 @@ pub(crate) struct Info { sat_ranges: u64, stored_bytes: u64, tables: BTreeMap, + total_bytes: u64, pub(crate) transactions: Vec, tree_height: u32, utxos_indexed: u64, @@ -133,7 +134,9 @@ pub(crate) struct TableInfo { fragmented_bytes: u64, leaf_pages: u64, metadata_bytes: u64, + proportion: f64, stored_bytes: u64, + total_bytes: u64, tree_height: u32, } @@ -444,18 +447,27 @@ impl Index { fn insert_table_info( tables: &mut BTreeMap, wtx: &WriteTransaction, + database_total_bytes: u64, definition: TableDefinition, ) { let stats = wtx.open_table(definition).unwrap().stats().unwrap(); + + let fragmented_bytes = stats.fragmented_bytes(); + let metadata_bytes = stats.metadata_bytes(); + let stored_bytes = stats.stored_bytes(); + let total_bytes = stored_bytes + metadata_bytes + fragmented_bytes; + tables.insert( definition.name().into(), TableInfo { - tree_height: stats.tree_height(), - leaf_pages: stats.leaf_pages(), branch_pages: stats.branch_pages(), - stored_bytes: stats.stored_bytes(), - metadata_bytes: stats.metadata_bytes(), - fragmented_bytes: stats.fragmented_bytes(), + fragmented_bytes, + leaf_pages: stats.leaf_pages(), + metadata_bytes, + proportion: total_bytes as f64 / database_total_bytes as f64, + stored_bytes, + total_bytes, + tree_height: stats.tree_height(), }, ); } @@ -463,6 +475,7 @@ impl Index { fn insert_multimap_table_info( tables: &mut BTreeMap, wtx: &WriteTransaction, + database_total_bytes: u64, definition: MultimapTableDefinition, ) { let stats = wtx @@ -470,15 +483,23 @@ impl Index { .unwrap() .stats() .unwrap(); + + let fragmented_bytes = stats.fragmented_bytes(); + let metadata_bytes = stats.metadata_bytes(); + let stored_bytes = stats.stored_bytes(); + let total_bytes = stored_bytes + metadata_bytes + fragmented_bytes; + tables.insert( definition.name().into(), TableInfo { - tree_height: stats.tree_height(), - leaf_pages: stats.leaf_pages(), branch_pages: stats.branch_pages(), - stored_bytes: stats.stored_bytes(), - metadata_bytes: stats.metadata_bytes(), - fragmented_bytes: stats.fragmented_bytes(), + fragmented_bytes, + leaf_pages: stats.leaf_pages(), + metadata_bytes, + proportion: total_bytes as f64 / database_total_bytes as f64, + stored_bytes, + total_bytes, + tree_height: stats.tree_height(), }, ); } @@ -487,31 +508,57 @@ impl Index { let stats = wtx.stats()?; + let fragmented_bytes = stats.fragmented_bytes(); + let metadata_bytes = stats.metadata_bytes(); + let stored_bytes = stats.stored_bytes(); + let total_bytes = fragmented_bytes + metadata_bytes + stored_bytes; + let mut tables: BTreeMap = BTreeMap::new(); - insert_multimap_table_info(&mut tables, &wtx, SATPOINT_TO_SEQUENCE_NUMBER); - insert_multimap_table_info(&mut tables, &wtx, SAT_TO_SEQUENCE_NUMBER); - insert_multimap_table_info(&mut tables, &wtx, SEQUENCE_NUMBER_TO_CHILDREN); - insert_table_info(&mut tables, &wtx, HEIGHT_TO_BLOCK_HASH); - insert_table_info(&mut tables, &wtx, HEIGHT_TO_BLOCK_HASH); - insert_table_info(&mut tables, &wtx, HEIGHT_TO_LAST_SEQUENCE_NUMBER); - insert_table_info(&mut tables, &wtx, HOME_INSCRIPTIONS); - insert_table_info(&mut tables, &wtx, INSCRIPTION_ID_TO_SEQUENCE_NUMBER); - insert_table_info(&mut tables, &wtx, INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER); - insert_table_info(&mut tables, &wtx, OUTPOINT_TO_RUNE_BALANCES); - insert_table_info(&mut tables, &wtx, OUTPOINT_TO_SAT_RANGES); - insert_table_info(&mut tables, &wtx, OUTPOINT_TO_VALUE); - insert_table_info(&mut tables, &wtx, RUNE_ID_TO_RUNE_ENTRY); - insert_table_info(&mut tables, &wtx, RUNE_TO_RUNE_ID); - insert_table_info(&mut tables, &wtx, SAT_TO_SATPOINT); - insert_table_info(&mut tables, &wtx, SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY); - insert_table_info(&mut tables, &wtx, SEQUENCE_NUMBER_TO_RUNE); - insert_table_info(&mut tables, &wtx, SEQUENCE_NUMBER_TO_SATPOINT); - insert_table_info(&mut tables, &wtx, STATISTIC_TO_COUNT); - insert_table_info(&mut tables, &wtx, TRANSACTION_ID_TO_RUNE); + insert_multimap_table_info(&mut tables, &wtx, total_bytes, SATPOINT_TO_SEQUENCE_NUMBER); + insert_multimap_table_info(&mut tables, &wtx, total_bytes, SAT_TO_SEQUENCE_NUMBER); + insert_multimap_table_info(&mut tables, &wtx, total_bytes, SEQUENCE_NUMBER_TO_CHILDREN); + insert_table_info(&mut tables, &wtx, total_bytes, HEIGHT_TO_BLOCK_HASH); + insert_table_info(&mut tables, &wtx, total_bytes, HEIGHT_TO_BLOCK_HASH); + insert_table_info( + &mut tables, + &wtx, + total_bytes, + HEIGHT_TO_LAST_SEQUENCE_NUMBER, + ); + insert_table_info(&mut tables, &wtx, total_bytes, HOME_INSCRIPTIONS); + insert_table_info( + &mut tables, + &wtx, + total_bytes, + INSCRIPTION_ID_TO_SEQUENCE_NUMBER, + ); insert_table_info( &mut tables, &wtx, + total_bytes, + INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER, + ); + insert_table_info(&mut tables, &wtx, total_bytes, OUTPOINT_TO_RUNE_BALANCES); + insert_table_info(&mut tables, &wtx, total_bytes, OUTPOINT_TO_SAT_RANGES); + insert_table_info(&mut tables, &wtx, total_bytes, OUTPOINT_TO_VALUE); + insert_table_info(&mut tables, &wtx, total_bytes, RUNE_ID_TO_RUNE_ENTRY); + insert_table_info(&mut tables, &wtx, total_bytes, RUNE_TO_RUNE_ID); + insert_table_info(&mut tables, &wtx, total_bytes, SAT_TO_SATPOINT); + insert_table_info( + &mut tables, + &wtx, + total_bytes, + SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY, + ); + insert_table_info(&mut tables, &wtx, total_bytes, SEQUENCE_NUMBER_TO_RUNE); + insert_table_info(&mut tables, &wtx, total_bytes, SEQUENCE_NUMBER_TO_SATPOINT); + insert_table_info(&mut tables, &wtx, total_bytes, STATISTIC_TO_COUNT); + insert_table_info(&mut tables, &wtx, total_bytes, TRANSACTION_ID_TO_RUNE); + insert_table_info( + &mut tables, + &wtx, + total_bytes, WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP, ); @@ -534,7 +581,6 @@ impl Index { .map(|x| x.value()) .unwrap_or(0); Info { - index_path: self.path.clone(), blocks_indexed: wtx .open_table(HEIGHT_TO_BLOCK_HASH)? .range(0..)? @@ -543,15 +589,17 @@ impl Index { .map(|(height, _hash)| height.value() + 1) .unwrap_or(0), branch_pages: stats.branch_pages(), - fragmented_bytes: stats.fragmented_bytes(), + fragmented_bytes, index_file_size: fs::metadata(&self.path)?.len(), + index_path: self.path.clone(), leaf_pages: stats.leaf_pages(), - metadata_bytes: stats.metadata_bytes(), - sat_ranges, + metadata_bytes, outputs_traversed, page_size: stats.page_size(), - stored_bytes: stats.stored_bytes(), + sat_ranges, + stored_bytes, tables, + total_bytes, transactions: wtx .open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)? .range(0..)? @@ -860,37 +908,63 @@ impl Index { Ok(runic) } - #[cfg(test)] - pub(crate) fn get_rune_balances(&self) -> Vec<(OutPoint, Vec<(RuneId, u128)>)> { + pub(crate) fn get_rune_balance_map(&self) -> Result>> { + let outpoint_balances = self.get_rune_balances()?; + + let rtx = self.database.begin_read()?; + + let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; + + let mut rune_balances: BTreeMap> = BTreeMap::new(); + + for (outpoint, balances) in outpoint_balances { + for (rune_id, amount) in balances { + let rune = RuneEntry::load( + rune_id_to_rune_entry + .get(&rune_id.store())? + .unwrap() + .value(), + ) + .rune; + + *rune_balances + .entry(rune) + .or_default() + .entry(outpoint) + .or_default() += amount; + } + } + + Ok(rune_balances) + } + + pub(crate) fn get_rune_balances(&self) -> Result)>> { let mut result = Vec::new(); for entry in self .database - .begin_read() - .unwrap() - .open_table(OUTPOINT_TO_RUNE_BALANCES) - .unwrap() - .iter() - .unwrap() + .begin_read()? + .open_table(OUTPOINT_TO_RUNE_BALANCES)? + .iter()? { - let (outpoint, balances_buffer) = entry.unwrap(); + let (outpoint, balances_buffer) = entry?; let outpoint = OutPoint::load(*outpoint.value()); let balances_buffer = balances_buffer.value(); let mut balances = Vec::new(); let mut i = 0; while i < balances_buffer.len() { - let (id, length) = runes::varint::decode(&balances_buffer[i..]).unwrap(); + let (id, length) = runes::varint::decode(&balances_buffer[i..])?; i += length; - let (balance, length) = runes::varint::decode(&balances_buffer[i..]).unwrap(); + let (balance, length) = runes::varint::decode(&balances_buffer[i..])?; i += length; - balances.push((RuneId::try_from(id).unwrap(), balance)); + balances.push((RuneId::try_from(id)?, balance)); } result.push((outpoint, balances)); } - result + Ok(result) } pub(crate) fn block_header(&self, hash: BlockHash) -> Result> { @@ -3453,7 +3527,6 @@ mod tests { let txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, witness)], - ..Default::default() }); diff --git a/src/index/testing.rs b/src/index/testing.rs index 36d580a8e6..d40405eae8 100644 --- a/src/index/testing.rs +++ b/src/index/testing.rs @@ -113,7 +113,7 @@ impl Context { assert_eq!(runes, self.index.runes().unwrap()); - assert_eq!(balances, self.index.get_rune_balances()); + assert_eq!(balances, self.index.get_rune_balances().unwrap()); let mut outstanding: HashMap = HashMap::new(); diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 5d4cb3a1c5..929c8fb75a 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -71,9 +71,10 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { ) -> Result { let mut envelopes = ParsedEnvelope::from_transaction(tx).into_iter().peekable(); let mut floating_inscriptions = Vec::new(); + let mut id_counter = 0; let mut inscribed_offsets = BTreeMap::new(); let mut total_input_value = 0; - let mut id_counter = 0; + let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); for (input_index, tx_in) in tx.input.iter().enumerate() { // skip subsidy since no inscriptions possible @@ -134,8 +135,6 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { index: id_counter, }; - let inscribed_offset = inscribed_offsets.get(&offset); - let curse = if self.height >= self.chain.jubilee_height() { None } else if inscription.payload.unrecognized_even_field { @@ -154,7 +153,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Some(Curse::Pushnum) } else if inscription.stutter { Some(Curse::Stutter) - } else if let Some((id, count)) = inscribed_offset { + } else if let Some((id, count)) = inscribed_offsets.get(&offset) { if *count > 1 { Some(Curse::Reinscription) } else { @@ -183,11 +182,17 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let unbound = current_input_value == 0 || curse == Some(Curse::UnrecognizedEvenField); + let offset = inscription + .payload + .pointer() + .filter(|&pointer| pointer < total_output_value) + .unwrap_or(offset); + floating_inscriptions.push(Flotsam { inscription_id, offset, origin: Origin::New { - reinscription: inscribed_offset.is_some(), + reinscription: inscribed_offsets.get(&offset).is_some(), cursed: curse.is_some(), fee: 0, hidden: inscription.payload.hidden(), @@ -197,6 +202,11 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { }, }); + inscribed_offsets + .entry(offset) + .or_insert((inscription_id, 0)) + .1 += 1; + envelopes.next(); id_counter += 1; } @@ -222,8 +232,6 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { } // still have to normalize over inscription size - let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); - for flotsam in &mut floating_inscriptions { if let Flotsam { origin: Origin::New { ref mut fee, .. }, diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 3c7f1b6ef0..6fe8dd0798 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -65,7 +65,7 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { let mut allocated: Vec> = vec![HashMap::new(); tx.output.len()]; if let Some(runestone) = runestone { - // Determine if this runestone conains a valid issuance + // Determine if this runestone contains a valid issuance let mut allocation = match runestone.etching { Some(etching) => { // If the issuance symbol is already taken, the issuance is ignored diff --git a/src/inscription.rs b/src/inscription.rs index b2e415204f..b517ba0d55 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -108,7 +108,7 @@ impl Inscription { }) } - fn pointer_value(pointer: u64) -> Vec { + pub(crate) fn pointer_value(pointer: u64) -> Vec { let mut bytes = pointer.to_le_bytes().to_vec(); while bytes.last().copied() == Some(0) { diff --git a/src/media.rs b/src/media.rs index a141ee1f1e..c4859bd38b 100644 --- a/src/media.rs +++ b/src/media.rs @@ -200,7 +200,7 @@ mod tests { } #[test] - fn no_duplicate_exensions() { + fn no_duplicate_extensions() { let mut set = HashSet::new(); for (_, _, _, extensions) in Media::TABLE { for extension in *extensions { diff --git a/src/subcommand.rs b/src/subcommand.rs index 2f33121e87..688923fc03 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -1,5 +1,6 @@ use super::*; +pub mod balances; pub mod decode; pub mod epochs; pub mod find; @@ -17,6 +18,8 @@ pub mod wallet; #[derive(Debug, Parser)] pub(crate) enum Subcommand { + #[command(about = "List all rune balances")] + Balances, #[command(about = "Decode a transaction")] Decode(decode::Decode), #[command(about = "List the first litoshis of each reward epoch")] @@ -50,6 +53,7 @@ pub(crate) enum Subcommand { impl Subcommand { pub(crate) fn run(self, options: Options) -> SubcommandResult { match self { + Self::Balances => balances::run(options), Self::Decode(decode) => decode.run(), Self::Epochs => epochs::run(), Self::Find(find) => find.run(options), diff --git a/src/subcommand/balances.rs b/src/subcommand/balances.rs new file mode 100644 index 0000000000..425d8a9db7 --- /dev/null +++ b/src/subcommand/balances.rs @@ -0,0 +1,21 @@ +use super::*; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Output { + pub runes: BTreeMap>, +} + +pub(crate) fn run(options: Options) -> SubcommandResult { + let index = Index::open(&options)?; + + ensure!( + index.has_rune_index(), + "`ord balances` requires index created with `--index-runes-pre-alpha-i-agree-to-get-rekt` flag", + ); + + index.update()?; + + Ok(Box::new(Output { + runes: index.get_rune_balance_map()?, + })) +} diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 7036e6498e..844288a23e 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -42,6 +42,8 @@ impl Preview { fs::create_dir(&litecoin_data_dir)?; + eprintln!("Spawning bitcoind…"); + let _bitcoind = KillOnDrop( Command::new("litecoind") .arg({ @@ -49,9 +51,10 @@ impl Preview { arg.push(&litecoin_data_dir); arg }) + .arg("-listen=0") + .arg("-printtoconsole=0") .arg("-regtest") .arg("-txindex") - .arg("-listen=0") .arg(format!("-rpcport={rpc_port}")) .spawn() .context("failed to spawn `litecoind`")?, @@ -75,7 +78,7 @@ impl Preview { panic!("Litecoin Core RPC did not respond"); } - thread::sleep(Duration::from_millis(50)); + thread::sleep(Duration::from_millis(100)); } super::wallet::Wallet::Create(super::wallet::create::Create { @@ -89,6 +92,8 @@ impl Preview { .get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32))? .require_network(Network::Regtest)?; + eprintln!("Mining blocks…"); + rpc_client.generate_to_address(101, &address)?; if let Some(files) = self.files { @@ -103,7 +108,7 @@ impl Preview { compress: false, destination: None, dry_run: false, - fee_rate: FeeRate::try_from(1.0).unwrap(), + fee_rate: FeeRate::try_from(2.0).unwrap(), file: Some(file), json_metadata: None, metaprotocol: None, @@ -135,7 +140,7 @@ impl Preview { compress: false, destination: None, dry_run: false, - fee_rate: FeeRate::try_from(1.0).unwrap(), + fee_rate: FeeRate::try_from(2.0).unwrap(), file: None, json_metadata: None, metaprotocol: None, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 2716a87121..35c190609c 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -2151,7 +2151,7 @@ mod tests { ); assert_eq!( - server.index.get_rune_balances(), + server.index.get_rune_balances().unwrap(), [(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])] ); @@ -2220,7 +2220,7 @@ mod tests { ); assert_eq!( - server.index.get_rune_balances(), + server.index.get_rune_balances().unwrap(), [(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])] ); @@ -2326,7 +2326,7 @@ mod tests { ); assert_eq!( - server.index.get_rune_balances(), + server.index.get_rune_balances().unwrap(), [(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])] ); @@ -2396,7 +2396,7 @@ mod tests { let output = OutPoint { txid, vout: 0 }; assert_eq!( - server.index.get_rune_balances(), + server.index.get_rune_balances().unwrap(), [(output, vec![(id, u128::max_value())])] ); @@ -4331,6 +4331,143 @@ next ); } + #[test] + fn charm_reinscription_in_same_tx_input() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(1); + + let script = script::Builder::new() + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice([1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice([]) + .push_slice(b"foo") + .push_opcode(opcodes::all::OP_ENDIF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice([1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice([]) + .push_slice(b"bar") + .push_opcode(opcodes::all::OP_ENDIF) + .push_opcode(opcodes::OP_FALSE) + .push_opcode(opcodes::all::OP_IF) + .push_slice(b"ord") + .push_slice([1]) + .push_slice(b"text/plain;charset=utf-8") + .push_slice([]) + .push_slice(b"qix") + .push_opcode(opcodes::all::OP_ENDIF) + .into_script(); + + let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0, witness)], + ..Default::default() + }); + + server.mine_blocks(1); + + let id = InscriptionId { txid, index: 0 }; + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription 0

.* +
+
id
+
{id}
+
output value
+ .* +
+.* +" + ), + ); + + let id = InscriptionId { txid, index: 1 }; + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + ".* + ♻️ + 👹.*", + ); + + let id = InscriptionId { txid, index: 2 }; + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + ".* + ♻️ + 👹.*", + ); + } + + #[test] + fn charm_reinscription_in_same_tx_with_pointer() { + let server = TestServer::new_with_regtest(); + + server.mine_blocks(3); + + let cursed_inscription = inscription("text/plain", "bar"); + let reinscription: Inscription = InscriptionTemplate { + pointer: Some(0), + ..Default::default() + } + .into(); + + let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[ + (1, 0, 0, inscription("text/plain", "foo").to_witness()), + (2, 0, 0, cursed_inscription.to_witness()), + (3, 0, 0, reinscription.to_witness()), + ], + ..Default::default() + }); + + server.mine_blocks(1); + + let id = InscriptionId { txid, index: 0 }; + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + format!( + ".*

Inscription 0

.* +
+
id
+
{id}
+
output value
+ .* +
+.* +" + ), + ); + + let id = InscriptionId { txid, index: 1 }; + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + ".* + 👹.*", + ); + + let id = InscriptionId { txid, index: 2 }; + server.assert_response_regex( + format!("/inscription/{id}"), + StatusCode::OK, + ".* + ♻️ + 👹.*", + ); + } + #[test] fn charm_unbound() { let server = TestServer::new_with_regtest(); diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 51c962a5a7..43f92e5c98 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -546,6 +546,7 @@ mod tests { let child_inscription = InscriptionTemplate { parent: Some(parent_inscription), + ..Default::default() } .into(); @@ -887,14 +888,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), ]; @@ -985,14 +989,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), ]; @@ -1060,14 +1067,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), ]; @@ -1227,14 +1237,17 @@ inscriptions: let inscriptions = vec![ InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), InscriptionTemplate { parent: Some(parent), + ..Default::default() } .into(), ]; diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 7dc70420fb..a8fa6d50cf 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -51,7 +51,7 @@ impl Batch { get_change_address(client, chain)?, ]; - let (commit_tx, reveal_tx, recovery_key_pair, total_fees) = self + let (commit_tx, reveal_tx, _recovery_key_pair, total_fees) = self .create_batch_inscription_transactions( wallet_inscriptions, chain, @@ -100,7 +100,7 @@ impl Batch { }; if !self.no_backup { - Self::backup_recovery_key(client, recovery_key_pair, chain.network())?; + // Self::backup_recovery_key(client, recovery_key_pair, chain.network())?; } let commit = client.send_raw_transaction(&signed_commit_tx)?; @@ -282,7 +282,12 @@ impl Batch { let commit_tx_address = Address::p2tr_tweaked(taproot_spend_info.output_key(), chain.network()); - let total_postage = self.postage * u64::try_from(self.inscriptions.len()).unwrap(); + let total_postage = match self.mode { + Mode::SameSat => self.postage, + Mode::SharedOutput | Mode::SeparateOutputs => { + self.postage * u64::try_from(self.inscriptions.len()).unwrap() + } + }; let mut reveal_inputs = vec![OutPoint::null()]; let mut reveal_outputs = self @@ -291,8 +296,8 @@ impl Batch { .map(|destination| TxOut { script_pubkey: destination.script_pubkey(), value: match self.mode { - Mode::SeparateOutputs | Mode::SameSat => self.postage.to_sat(), - Mode::SharedOutput => total_postage.to_sat(), + Mode::SeparateOutputs => self.postage.to_sat(), + Mode::SharedOutput | Mode::SameSat => total_postage.to_sat(), }, }) .collect::>(); @@ -439,6 +444,7 @@ impl Batch { Ok((unsigned_commit_tx, reveal_tx, recovery_key_pair, total_fees)) } + #[allow(dead_code)] fn backup_recovery_key( client: &Client, recovery_key_pair: TweakedKeyPair, diff --git a/src/test.rs b/src/test.rs index 60d98c1dad..a306c19bba 100644 --- a/src/test.rs +++ b/src/test.rs @@ -114,12 +114,14 @@ pub(crate) fn tx_out(value: u64, address: Address) -> TxOut { #[derive(Default, Debug)] pub(crate) struct InscriptionTemplate { pub(crate) parent: Option, + pub(crate) pointer: Option, } impl From for Inscription { fn from(template: InscriptionTemplate) -> Self { Self { parent: template.parent.map(|id| id.parent_value()), + pointer: template.pointer.map(Inscription::pointer_value), ..Default::default() } } diff --git a/test-bitcoincore-rpc/src/server.rs b/test-bitcoincore-rpc/src/server.rs index 39a9c24560..e282bcb33e 100644 --- a/test-bitcoincore-rpc/src/server.rs +++ b/test-bitcoincore-rpc/src/server.rs @@ -253,10 +253,19 @@ impl Api for Server { .map(|txout| txout.value) .sum::(); - let (outpoint, input_value) = state + let mut utxos = state .utxos + .clone() + .into_iter() + .map(|(outpoint, value)| (value, outpoint)) + .collect::>(); + + utxos.sort(); + utxos.reverse(); + + let (input_value, outpoint) = utxos .iter() - .find(|(outpoint, value)| value.to_sat() >= output_value && !state.locked.contains(outpoint)) + .find(|(value, outpoint)| value.to_sat() >= output_value && !state.locked.contains(outpoint)) .ok_or_else(Self::not_found)?; transaction.input.push(TxIn { diff --git a/tests/balances.rs b/tests/balances.rs new file mode 100644 index 0000000000..247e96a15a --- /dev/null +++ b/tests/balances.rs @@ -0,0 +1,86 @@ +use {super::*, ord::subcommand::balances::Output}; + +#[test] +fn flag_is_required() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + CommandBuilder::new("--regtest balances") + .rpc_server(&rpc_server) + .expected_exit_code(1) + .expected_stderr( + "error: `ord balances` requires index created with `--index-runes-pre-alpha-i-agree-to-get-rekt` flag\n", + ) + .run_and_extract_stdout(); +} + +#[test] +fn no_runes() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let output = + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt balances") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!( + output, + Output { + runes: BTreeMap::new() + } + ); +} + +#[test] +fn with_runes() { + let rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + create_wallet(&rpc_server); + + let a = etch(&rpc_server, Rune(RUNE)); + let b = etch(&rpc_server, Rune(RUNE + 1)); + + let output = + CommandBuilder::new("--regtest --index-runes-pre-alpha-i-agree-to-get-rekt balances") + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + assert_eq!( + output, + Output { + runes: vec![ + ( + Rune(RUNE), + vec![( + OutPoint { + txid: a.transaction, + vout: 1 + }, + 1000 + )] + .into_iter() + .collect() + ), + ( + Rune(RUNE + 1), + vec![( + OutPoint { + txid: b.transaction, + vout: 1 + }, + 1000 + )] + .into_iter() + .collect() + ), + ] + .into_iter() + .collect(), + } + ); +} diff --git a/tests/info.rs b/tests/info.rs index fb431f267c..1ee0ac29da 100644 --- a/tests/info.rs +++ b/tests/info.rs @@ -19,6 +19,7 @@ fn json_with_satoshi_index() { "sat_ranges": 1, "stored_bytes": \d+, "tables": .*, + "total_bytes": \d+, "transactions": \[ \{ "starting_block_count": 0, @@ -52,6 +53,7 @@ fn json_without_satoshi_index() { "sat_ranges": 0, "stored_bytes": \d+, "tables": .*, + "total_bytes": \d+, "transactions": \[ \{ "starting_block_count": 0, diff --git a/tests/lib.rs b/tests/lib.rs index 2004566e76..4026234ed2 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -123,6 +123,7 @@ mod command_builder; mod expected; mod test_server; +mod balances; mod core; mod decode; mod epochs; diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 14a6dd9d20..fe77f6e4da 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -1688,3 +1688,56 @@ fn batch_inscribe_with_sat_arg_fails_if_wrong_mode() { .expected_stderr("error: `sat` can only be set in `same-sat` mode\n") .run_and_extract_stdout(); } + +#[test] +fn batch_inscribe_with_fee_rate() { + let rpc_server = test_bitcoincore_rpc::spawn(); + create_wallet(&rpc_server); + rpc_server.mine_blocks(2); + + let set_fee_rate = 1.0; + + let output = CommandBuilder::new(format!("--index-sats wallet inscribe --fee-rate {set_fee_rate} --batch batch.yaml")) + .write("inscription.txt", "Hello World") + .write("tulip.png", [0; 555]) + .write("meow.wav", [0; 2048]) + .write( + "batch.yaml", + "mode: same-sat\nsat: 5000111111\ninscriptions:\n- file: inscription.txt\n- file: tulip.png\n- file: meow.wav\n" + ) + .rpc_server(&rpc_server) + .run_and_deserialize_output::(); + + let commit_tx = &rpc_server.mempool()[0]; + let mut fee = 0; + for input in &commit_tx.input { + fee += rpc_server + .get_utxo_amount(&input.previous_output) + .unwrap() + .to_sat(); + } + for output in &commit_tx.output { + fee -= output.value; + } + let fee_rate = fee as f64 / commit_tx.vsize() as f64; + pretty_assert_eq!(fee_rate, set_fee_rate); + + let reveal_tx = &rpc_server.mempool()[1]; + let mut fee = 0; + for input in &reveal_tx.input { + fee += &commit_tx.output[input.previous_output.vout as usize].value; + } + for output in &reveal_tx.output { + fee -= output.value; + } + let fee_rate = fee as f64 / reveal_tx.vsize() as f64; + pretty_assert_eq!(fee_rate, set_fee_rate); + + assert_eq!( + ord::FeeRate::try_from(set_fee_rate) + .unwrap() + .fee(commit_tx.vsize() + reveal_tx.vsize()) + .to_sat(), + output.total_fees + ); +}