diff --git a/Cargo.lock b/Cargo.lock index 7707d87531..39195ca8eb 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" @@ -694,6 +702,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" diff --git a/Cargo.toml b/Cargo.toml index 1521e1d377..7095fe5975 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/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 ff11188b93..287e001aa1 100644 --- a/src/index.rs +++ b/src/index.rs @@ -901,37 +901,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> { 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/subcommand.rs b/src/subcommand.rs index 89de4b3b37..16dc0910f5 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 satoshis 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/server.rs b/src/subcommand/server.rs index c943209b6d..0452c231f7 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())])] ); diff --git a/test-bitcoincore-rpc/src/server.rs b/test-bitcoincore-rpc/src/server.rs index c4edeef079..6afa9f00f2 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/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;