diff --git a/Cargo.lock b/Cargo.lock index 2cf0f16846..5181cee173 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,6 +2158,7 @@ dependencies = [ "rustls-acme", "serde", "serde_json", + "serde_yaml", "sys-info", "tempfile", "test-bitcoincore-rpc", @@ -2952,6 +2953,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.6.1" @@ -3588,6 +3602,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 63b2b3286e..a10c6762de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ rustls = "0.20.6" rustls-acme = { version = "0.5.0", features = ["axum"] } serde = { version = "1.0.137", features = ["derive"] } serde_json = { version = "1.0.81" } +serde_yaml = "0.9.17" sys-info = "0.9.1" tempfile = "3.2.0" tokio = { version = "1.17.0", features = ["rt-multi-thread"] } diff --git a/deploy/ord.service b/deploy/ord.service index 47a57e960c..e24060d518 100644 --- a/deploy/ord.service +++ b/deploy/ord.service @@ -11,6 +11,7 @@ Environment=RUST_LOG=info ExecStart=/usr/local/bin/ord \ --bitcoin-data-dir /var/lib/bitcoind \ --data-dir /var/lib/ord \ + --config /var/lib/ord/ord.yaml \ --chain ${CHAIN} \ --index-sats \ server \ diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000000..fdc37be10e --- /dev/null +++ b/src/config.rs @@ -0,0 +1,35 @@ +use super::*; + +#[derive(Deserialize, Default, PartialEq, Debug)] +pub(crate) struct Config { + pub(crate) hidden: HashSet, +} + +impl Config { + pub(crate) fn is_hidden(&self, inscription_id: InscriptionId) -> bool { + self.hidden.contains(&inscription_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inscriptions_can_be_hidden() { + let a = "8d363b28528b0cb86b5fd48615493fb175bdf132d2a3d20b4251bba3f130a5abi0" + .parse::() + .unwrap(); + + let b = "8d363b28528b0cb86b5fd48615493fb175bdf132d2a3d20b4251bba3f130a5abi1" + .parse::() + .unwrap(); + + let config = Config { + hidden: iter::once(a).collect(), + }; + + assert!(config.is_hidden(a)); + assert!(!config.is_hidden(b)); + } +} diff --git a/src/inscription_id.rs b/src/inscription_id.rs index fe5e418e88..5ff9a347c4 100644 --- a/src/inscription_id.rs +++ b/src/inscription_id.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)] pub struct InscriptionId { pub(crate) txid: Txid, pub(crate) index: u32, diff --git a/src/lib.rs b/src/lib.rs index 15d40e1478..0b6266180d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ use { self::{ arguments::Arguments, blocktime::Blocktime, + config::Config, decimal::Decimal, degree::Degree, deserialize_from_str::DeserializeFromStr, @@ -53,7 +54,8 @@ use { env, ffi::OsString, fmt::{self, Display, Formatter}, - fs, io, + fs::{self, File}, + io, net::{TcpListener, ToSocketAddrs}, ops::{Add, AddAssign, Sub}, path::{Path, PathBuf}, @@ -95,6 +97,7 @@ macro_rules! tprintln { mod arguments; mod blocktime; mod chain; +mod config; mod decimal; mod degree; mod deserialize_from_str; diff --git a/src/options.rs b/src/options.rs index 83a5f2c23e..ea8ce585e0 100644 --- a/src/options.rs +++ b/src/options.rs @@ -16,6 +16,8 @@ pub(crate) struct Options { help = "Use ." )] pub(crate) chain_argument: Chain, + #[clap(long, help = "Load configuration from .")] + pub(crate) config: Option, #[clap(long, help = "Load Bitcoin Core RPC cookie file from .")] pub(crate) cookie_file: Option, #[clap(long, help = "Store index in .")] @@ -111,6 +113,13 @@ impl Options { Ok(self.chain().join_with_data_dir(&base)) } + pub(crate) fn load_config(&self) -> Result { + match &self.config { + Some(path) => Ok(serde_yaml::from_reader(File::open(path)?)?), + None => Ok(Default::default()), + } + } + fn format_bitcoin_core_version(version: usize) -> String { format!( "{}.{}.{}", @@ -507,4 +516,38 @@ mod tests { "foo" ) } + + #[test] + fn default_config_is_returned_if_config_option_is_not_passed() { + assert_eq!( + Arguments::try_parse_from(["ord", "index"]) + .unwrap() + .options + .load_config() + .unwrap(), + Default::default() + ); + } + + #[test] + fn config_is_loaded_from_config_option_path() { + let id = "8d363b28528b0cb86b5fd48615493fb175bdf132d2a3d20b4251bba3f130a5abi0" + .parse::() + .unwrap(); + + let tempdir = TempDir::new().unwrap(); + let path = tempdir.path().join("ord.yaml"); + fs::write(&path, format!("hidden:\n- \"{id}\"")).unwrap(); + + assert_eq!( + Arguments::try_parse_from(["ord", "--config", path.to_str().unwrap(), "index",]) + .unwrap() + .options + .load_config() + .unwrap(), + Config { + hidden: iter::once(id).collect(), + } + ); + } } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 4c00ce1dda..3be45543fa 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -133,6 +133,8 @@ impl Server { thread::sleep(Duration::from_millis(5000)); }); + let config = options.load_config()?; + let router = Router::new() .route("/", get(Self::home)) .route("/block-count", get(Self::block_count)) @@ -161,6 +163,7 @@ impl Server { .route("/tx/:txid", get(Self::transaction)) .layer(Extension(index)) .layer(Extension(options.chain())) + .layer(Extension(Arc::new(config))) .layer(SetResponseHeaderLayer::if_not_present( header::CONTENT_SECURITY_POLICY, HeaderValue::from_static("default-src 'self'"), @@ -705,8 +708,13 @@ impl Server { async fn content( Extension(index): Extension>, + Extension(config): Extension>, Path(inscription_id): Path, ) -> ServerResult { + if config.is_hidden(inscription_id) { + return Ok(PreviewUnknownHtml.into_response()); + } + let inscription = index .get_inscription_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; @@ -743,8 +751,13 @@ impl Server { async fn preview( Extension(index): Extension>, + Extension(config): Extension>, Path(inscription_id): Path, ) -> ServerResult { + if config.is_hidden(inscription_id) { + return Ok(PreviewUnknownHtml.into_response()); + } + let inscription = index .get_inscription_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; @@ -915,8 +928,22 @@ mod tests { } fn new_with_args(ord_args: &[&str], server_args: &[&str]) -> Self { - let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + Self::new_server(test_bitcoincore_rpc::spawn(), None, ord_args, server_args) + } + + fn new_with_bitcoin_rpc_server_and_config( + bitcoin_rpc_server: test_bitcoincore_rpc::Handle, + config: String, + ) -> Self { + Self::new_server(bitcoin_rpc_server, Some(config), &[], &[]) + } + fn new_server( + bitcoin_rpc_server: test_bitcoincore_rpc::Handle, + config: Option, + ord_args: &[&str], + server_args: &[&str], + ) -> Self { let tempdir = TempDir::new().unwrap(); let cookiefile = tempdir.path().join("cookie"); @@ -931,8 +958,17 @@ mod tests { let url = Url::parse(&format!("http://127.0.0.1:{port}")).unwrap(); + let config_args = match config { + Some(config) => { + let config_path = tempdir.path().join("ord.yaml"); + fs::write(&config_path, config).unwrap(); + format!("--config {}", config_path.display()) + } + None => "".to_string(), + }; + let (options, server) = parse_server_args(&format!( - "ord --chain regtest --rpc-url {} --cookie-file {} --data-dir {} {} server --http-port {} --address 127.0.0.1 {}", + "ord --chain regtest --rpc-url {} --cookie-file {} --data-dir {} {config_args} {} server --http-port {} --address 127.0.0.1 {}", bitcoin_rpc_server.url(), cookiefile.to_str().unwrap(), tempdir.path().to_str().unwrap(), @@ -2400,4 +2436,34 @@ mod tests { "br" ); } + + #[test] + fn inscriptions_can_be_hidden_with_config() { + let bitcoin_rpc_server = test_bitcoincore_rpc::spawn(); + bitcoin_rpc_server.mine_blocks(1); + let txid = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(1, 0, 0)], + witness: inscription("text/plain;charset=utf-8", "hello").to_witness(), + ..Default::default() + }); + let inscription = InscriptionId::from(txid); + bitcoin_rpc_server.mine_blocks(1); + + let server = TestServer::new_with_bitcoin_rpc_server_and_config( + bitcoin_rpc_server, + format!("\"hidden\":\n - {inscription}"), + ); + + server.assert_response( + format!("/preview/{inscription}"), + StatusCode::OK, + &fs::read_to_string("templates/preview-unknown.html").unwrap(), + ); + + server.assert_response( + format!("/content/{inscription}"), + StatusCode::OK, + &fs::read_to_string("templates/preview-unknown.html").unwrap(), + ); + } } diff --git a/src/test.rs b/src/test.rs index 35f099d172..27a8d45f83 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,5 +1,5 @@ pub(crate) use { - super::*, bitcoin::Witness, pretty_assertions::assert_eq as pretty_assert_eq, + super::*, bitcoin::Witness, pretty_assertions::assert_eq as pretty_assert_eq, std::iter, test_bitcoincore_rpc::TransactionTemplate, unindent::Unindent, }; diff --git a/tests/server.rs b/tests/server.rs index bca11d144a..1510cf1bac 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -173,7 +173,7 @@ fn inscription_content() { rpc_server.mine_blocks(1); let response = - TestServer::spawn_with_args(&rpc_server, &[]).request(&format!("/content/{inscription}")); + TestServer::spawn_with_args(&rpc_server, &[]).request(format!("/content/{inscription}")); assert_eq!(response.status(), StatusCode::OK); assert_eq!( diff --git a/tests/test_server.rs b/tests/test_server.rs index 3133ee9c64..d99fb3ff93 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -79,7 +79,7 @@ impl TestServer { assert_regex_match!(response.text().unwrap(), regex.as_ref()); } - pub(crate) fn request(&self, path: &str) -> Response { + pub(crate) fn request(&self, path: impl AsRef) -> Response { let client = Client::new(&self.rpc_url, Auth::None).unwrap(); let chain_block_count = client.get_block_count().unwrap() + 1; @@ -94,7 +94,7 @@ impl TestServer { thread::sleep(Duration::from_millis(25)); } - reqwest::blocking::get(self.url().join(path).unwrap()).unwrap() + reqwest::blocking::get(self.url().join(path.as_ref()).unwrap()).unwrap() } } diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index 1e47984ef9..fa3b0b821d 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -14,7 +14,7 @@ fn inscribe_creates_inscriptions() { assert_eq!(rpc_server.descriptors().len(), 3); let request = - TestServer::spawn_with_args(&rpc_server, &[]).request(&format!("/content/{inscription}")); + TestServer::spawn_with_args(&rpc_server, &[]).request(format!("/content/{inscription}")); assert_eq!(request.status(), 200); assert_eq!(