1use {
2 self::{
3 accept_encoding::AcceptEncoding,
4 accept_json::AcceptJson,
5 error::{OptionExt, ServerError, ServerResult},
6 },
7 super::*,
8 crate::templates::{
9 AddressHtml, BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, GalleriesHtml,
10 GalleryHtml, HomeHtml, InputHtml, InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml,
11 ItemHtml, OutputHtml, PageContent, PageHtml, ParentsHtml, PreviewAudioHtml, PreviewCodeHtml,
12 PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml,
13 PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RareTxt, RuneHtml, RuneNotFoundHtml,
14 RunesHtml, SatHtml, SatscardHtml, TransactionHtml,
15 },
16 axum::{
17 Router,
18 extract::{DefaultBodyLimit, Extension, Json, Path, Query},
19 http::{self, HeaderMap, HeaderName, HeaderValue, StatusCode, Uri, header},
20 response::{IntoResponse, Redirect, Response},
21 routing::{get, post},
22 },
23 axum_server::Handle,
24 rust_embed::RustEmbed,
25 rustls_acme::{
26 AcmeConfig,
27 acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY},
28 axum::AxumAcceptor,
29 caches::DirCache,
30 },
31 tokio_stream::StreamExt,
32 tower_http::{
33 compression::CompressionLayer,
34 cors::{Any, CorsLayer},
35 set_header::SetResponseHeaderLayer,
36 validate_request::ValidateRequestHeaderLayer,
37 },
38};
39
40pub use server_config::ServerConfig;
41
42mod accept_encoding;
43mod accept_json;
44mod error;
45pub mod query;
46mod r;
47mod server_config;
48
49const MEBIBYTE: usize = 1 << 20;
50const PAGE_SIZE: usize = 100;
51
52enum SpawnConfig {
53 Https(AxumAcceptor),
54 Http,
55 Redirect(String),
56}
57
58#[derive(Deserialize)]
59pub(crate) struct OutputsQuery {
60 #[serde(rename = "type")]
61 pub(crate) ty: Option<OutputType>,
62}
63
64#[derive(Clone, Copy, Deserialize, Default, PartialEq)]
65#[serde(rename_all = "lowercase")]
66pub(crate) enum OutputType {
67 #[default]
68 Any,
69 Cardinal,
70 Inscribed,
71 Runic,
72}
73
74#[derive(Deserialize)]
75struct Search {
76 query: String,
77}
78
79#[derive(RustEmbed)]
80#[folder = "static"]
81struct StaticAssets;
82
83static SAT_AT_INDEX_PATH: LazyLock<Regex> =
84 LazyLock::new(|| Regex::new(r"^/r/sat/[^/]+/at/[^/]+$").unwrap());
85
86#[derive(Debug, Parser, Clone)]
87pub struct Server {
88 #[arg(long, help = "Accept PSBT offer submissions to server.")]
89 pub(crate) accept_offers: bool,
90 #[arg(
91 long,
92 help = "Listen on <ADDRESS> for incoming requests. [default: 0.0.0.0]"
93 )]
94 pub(crate) address: Option<String>,
95 #[arg(
96 long,
97 help = "Request ACME TLS certificate for <ACME_DOMAIN>. This ord instance must be reachable at <ACME_DOMAIN>:443 to respond to Let's Encrypt ACME challenges."
98 )]
99 pub(crate) acme_domain: Vec<String>,
100 #[arg(
101 long,
102 help = "Use <CSP_ORIGIN> in Content-Security-Policy header. Set this to the public-facing URL of your ord instance."
103 )]
104 pub(crate) csp_origin: Option<String>,
105 #[arg(
106 long,
107 help = "Decompress encoded content. Currently only supports brotli. Be careful using this on production instances. A decompressed inscription may be arbitrarily large, making decompression a DoS vector."
108 )]
109 pub(crate) decompress: bool,
110 #[arg(long, env = "ORD_SERVER_DISABLE_JSON_API", help = "Disable JSON API.")]
111 pub(crate) disable_json_api: bool,
112 #[arg(
113 long,
114 help = "Listen on <HTTP_PORT> for incoming HTTP requests. [default: 80]"
115 )]
116 pub(crate) http_port: Option<u16>,
117 #[arg(
118 long,
119 group = "port",
120 help = "Listen on <HTTPS_PORT> for incoming HTTPS requests. [default: 443]"
121 )]
122 pub(crate) https_port: Option<u16>,
123 #[arg(long, help = "Store ACME TLS certificates in <ACME_CACHE>.")]
124 pub(crate) acme_cache: Option<PathBuf>,
125 #[arg(long, help = "Provide ACME contact <ACME_CONTACT>.")]
126 pub(crate) acme_contact: Vec<String>,
127 #[arg(long, help = "Serve HTTP traffic on <HTTP_PORT>.")]
128 pub(crate) http: bool,
129 #[arg(long, help = "Serve HTTPS traffic on <HTTPS_PORT>.")]
130 pub(crate) https: bool,
131 #[arg(long, help = "Redirect HTTP traffic to HTTPS.")]
132 pub(crate) redirect_http_to_https: bool,
133 #[arg(long, alias = "nosync", help = "Do not update the index.")]
134 pub(crate) no_sync: bool,
135 #[arg(
136 long,
137 help = "Proxy `/content/INSCRIPTION_ID` and other recursive endpoints to `<PROXY>` if the inscription is not present on current chain."
138 )]
139 pub(crate) proxy: Option<Url>,
140 #[arg(
141 long,
142 default_value = "5s",
143 help = "Poll Bitcoin Core every <POLLING_INTERVAL>."
144 )]
145 pub(crate) polling_interval: humantime::Duration,
146}
147
148impl Server {
149 pub fn run(
150 self,
151 settings: Settings,
152 index: Arc<Index>,
153 handle: Handle<SocketAddr>,
154 http_port_tx: Option<std::sync::mpsc::Sender<u16>>,
155 ) -> SubcommandResult {
156 settings.runtime()?.block_on(async {
157 let index_clone = index.clone();
158 let integration_test = settings.integration_test();
159
160 if (cfg!(test) || integration_test) && !self.no_sync {
161 index.update().unwrap();
162 }
163
164 let index_thread = thread::spawn(move || {
165 loop {
166 if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
167 break;
168 }
169
170 if !self.no_sync
171 && let Err(error) = index_clone.update()
172 {
173 log::warn!("Updating index: {error}");
174 }
175
176 thread::sleep(if integration_test {
177 Duration::from_millis(100)
178 } else {
179 self.polling_interval.into()
180 });
181 }
182 });
183
184 INDEXER.lock().unwrap().replace(index_thread);
185
186 let settings = Arc::new(settings);
187 let acme_domains = self.acme_domains()?;
188
189 let server_config = Arc::new(ServerConfig {
190 accept_offers: self.accept_offers,
191 chain: settings.chain(),
192 csp_origin: self.csp_origin.clone(),
193 decompress: self.decompress,
194 domain: acme_domains.first().cloned(),
195 index_sats: index.has_sat_index(),
196 json_api_enabled: !self.disable_json_api,
197 proxy: self.proxy.clone(),
198 });
199
200 let body_limit = if server_config.json_api_enabled {
201 DefaultBodyLimit::max(32 * MEBIBYTE)
202 } else {
203 DefaultBodyLimit::max(2 * MEBIBYTE)
204 };
205
206 let router = Router::new()
208 .route("/", get(Self::home))
209 .route("/address/{address}", get(Self::address))
210 .route("/block/{query}", get(Self::block))
211 .route("/blockcount", get(Self::block_count))
212 .route("/blocks", get(Self::blocks))
213 .route("/bounties", get(Self::bounties))
214 .route("/children/{inscription_id}", get(Self::children))
215 .route(
216 "/children/{inscription_id}/{page}",
217 get(Self::children_paginated),
218 )
219 .route("/clock", get(Self::clock))
220 .route("/collections", get(Self::collections))
221 .route("/collections/{page}", get(Self::collections_paginated))
222 .route("/decode/{txid}", get(Self::decode))
223 .route("/galleries", get(Self::galleries))
224 .route("/galleries/{page}", get(Self::galleries_paginated))
225 .route("/faq", get(Self::faq))
226 .route("/favicon.ico", get(Self::favicon))
227 .route("/feed.xml", get(Self::feed))
228 .route("/gallery/{inscription_id}", get(Self::gallery))
229 .route(
230 "/gallery/{inscription_id}/page/{page}",
231 get(Self::gallery_paginated),
232 )
233 .route("/gallery/{inscription_query}/{item}", get(Self::item))
234 .route("/input/{block}/{transaction}/{input}", get(Self::input))
235 .route("/inscription/{inscription_query}", get(Self::inscription))
236 .route(
237 "/inscription/{inscription_query}/{child}",
238 get(Self::inscription_child),
239 )
240 .route("/inscriptions", get(Self::inscriptions))
241 .route(
242 "/inscriptions",
243 post(Self::inscriptions_json).layer(body_limit),
244 )
245 .route(
246 "/inscriptions/block/{height}",
247 get(Self::inscriptions_in_block),
248 )
249 .route(
250 "/inscriptions/block/{height}/{page}",
251 get(Self::inscriptions_in_block_paginated),
252 )
253 .route("/inscriptions/{page}", get(Self::inscriptions_paginated))
254 .route("/install.sh", get(Self::install_script))
255 .route("/missing", post(Self::missing).layer(body_limit))
256 .route("/offer", post(Self::offer))
257 .route("/offers", get(Self::offers))
258 .route("/ordinal/{sat}", get(Self::ordinal))
259 .route("/output/{output}", get(Self::output))
260 .route("/outputs", post(Self::outputs).layer(body_limit))
261 .route("/outputs/{address}", get(Self::outputs_address))
262 .route("/parents/{inscription_id}", get(Self::parents))
263 .route(
264 "/parents/{inscription_id}/{page}",
265 get(Self::parents_paginated),
266 )
267 .route("/preview/{inscription_id}", get(Self::preview))
268 .route("/rare.txt", get(Self::rare_txt))
269 .route("/rune/{rune}", get(Self::rune))
270 .route("/runes", get(Self::runes))
271 .route("/runes/{page}", get(Self::runes_paginated))
272 .route("/sat/{sat}", get(Self::sat))
273 .route("/satpoint/{satpoint}", get(Self::satpoint))
274 .route("/satscard", get(Self::satscard))
275 .route("/search", get(Self::search_by_query))
276 .route("/search/{*query}", get(Self::search_by_path))
277 .route("/static/{*path}", get(Self::static_asset))
278 .route("/status", get(Self::status))
279 .route("/tx/{txid}", get(Self::transaction))
280 .route("/update", get(Self::update));
281
282 let router = router
284 .route("/blockhash", get(r::blockhash_string))
285 .route("/blockhash/{height}", get(r::block_hash_from_height_string))
286 .route("/blockheight", get(r::blockheight_string))
287 .route("/blocktime", get(r::blocktime_string))
288 .route("/r/blockhash", get(r::blockhash))
289 .route("/r/blockhash/{height}", get(r::blockhash_at_height))
290 .route("/r/blockheight", get(r::blockheight_string))
291 .route("/r/blockinfo/{query}", get(r::blockinfo))
292 .route("/r/blocktime", get(r::blocktime_string))
293 .route(
294 "/r/children/{inscription_id}/inscriptions",
295 get(r::children_inscriptions),
296 )
297 .route(
298 "/r/children/{inscription_id}/inscriptions/{page}",
299 get(r::children_inscriptions_paginated),
300 )
301 .route("/r/parents/{inscription_id}", get(r::parents))
302 .route(
303 "/r/parents/{inscription_id}/{page}",
304 get(r::parents_paginated),
305 )
306 .route(
307 "/r/parents/{inscription_id}/inscriptions",
308 get(r::parent_inscriptions),
309 )
310 .route(
311 "/r/parents/{inscription_id}/inscriptions/{page}",
312 get(r::parent_inscriptions_paginated),
313 )
314 .route("/r/sat/{sat_number}", get(r::sat))
315 .route("/r/sat/{sat_number}/{page}", get(r::sat_paginated))
316 .route("/r/tx/{txid}", get(r::tx))
317 .route(
318 "/r/undelegated-content/{inscription_id}",
319 get(r::undelegated_content),
320 )
321 .route("/r/utxo/{outpoint}", get(r::utxo));
322
323 let proxiable_routes = Router::new()
324 .route("/content/{inscription_id}", get(r::content))
325 .route("/r/children/{inscription_id}", get(r::children))
326 .route(
327 "/r/children/{inscription_id}/{page}",
328 get(r::children_paginated),
329 )
330 .route("/r/inscription/{inscription_id}", get(r::inscription))
331 .route("/r/metadata/{inscription_id}", get(r::metadata))
332 .route("/r/sat/{sat_number}/at/{index}", get(r::sat_at_index))
333 .route(
334 "/r/sat/{sat_number}/at/{index}/content",
335 get(r::sat_at_index_content),
336 )
337 .layer(axum::middleware::from_fn(Self::proxy_layer));
338
339 let router = router.merge(proxiable_routes);
340
341 let router = router
342 .fallback(Self::fallback)
343 .layer(Extension(index))
344 .layer(Extension(server_config.clone()))
345 .layer(Extension(settings.clone()))
346 .layer(SetResponseHeaderLayer::if_not_present(
347 header::CONTENT_SECURITY_POLICY,
348 HeaderValue::from_static("default-src 'self'"),
349 ))
350 .layer(SetResponseHeaderLayer::overriding(
351 header::STRICT_TRANSPORT_SECURITY,
352 HeaderValue::from_static("max-age=31536000; includeSubDomains; preload"),
353 ))
354 .layer(
355 CorsLayer::new()
356 .allow_methods([http::Method::GET, http::Method::POST])
357 .allow_headers([http::header::CONTENT_TYPE])
358 .allow_origin(Any),
359 )
360 .layer(CompressionLayer::new())
361 .with_state(server_config.clone());
362
363 let router = if let Some((username, password)) = settings.credentials() {
364 #[allow(deprecated)]
365 router.layer(ValidateRequestHeaderLayer::basic(username, password))
366 } else {
367 router
368 };
369
370 match (self.http_port(), self.https_port()) {
371 (Some(http_port), None) => {
372 self
373 .spawn(
374 &settings,
375 router,
376 handle,
377 http_port,
378 SpawnConfig::Http,
379 http_port_tx,
380 )?
381 .await??
382 }
383 (None, Some(https_port)) => {
384 self
385 .spawn(
386 &settings,
387 router,
388 handle,
389 https_port,
390 SpawnConfig::Https(self.acceptor(&settings)?),
391 None,
392 )?
393 .await??
394 }
395 (Some(http_port), Some(https_port)) => {
396 let http_spawn_config = if self.redirect_http_to_https {
397 SpawnConfig::Redirect(if https_port == 443 {
398 format!("https://{}", acme_domains[0])
399 } else {
400 format!("https://{}:{https_port}", acme_domains[0])
401 })
402 } else {
403 SpawnConfig::Http
404 };
405
406 let (http_result, https_result) = tokio::join!(
407 self.spawn(
408 &settings,
409 router.clone(),
410 handle.clone(),
411 http_port,
412 http_spawn_config,
413 http_port_tx,
414 )?,
415 self.spawn(
416 &settings,
417 router,
418 handle,
419 https_port,
420 SpawnConfig::Https(self.acceptor(&settings)?),
421 None,
422 )?
423 );
424 http_result.and(https_result)??;
425 }
426 (None, None) => unreachable!(),
427 }
428
429 Ok(None)
430 })
431 }
432
433 fn spawn(
434 &self,
435 settings: &Settings,
436 router: Router,
437 handle: Handle<SocketAddr>,
438 port: u16,
439 config: SpawnConfig,
440 port_tx: Option<std::sync::mpsc::Sender<u16>>,
441 ) -> Result<task::JoinHandle<io::Result<()>>> {
442 let address = match &self.address {
443 Some(address) => address.as_str(),
444 None => {
445 if cfg!(test) || settings.integration_test() {
446 "127.0.0.1"
447 } else {
448 "0.0.0.0"
449 }
450 }
451 };
452
453 let addr = (address, port)
454 .to_socket_addrs()?
455 .next()
456 .ok_or_else(|| anyhow!("failed to get socket addrs"))?;
457
458 let test = settings.integration_test() || cfg!(test);
459
460 Ok(tokio::spawn(async move {
461 let listener = tokio::net::TcpListener::bind(addr).await?.into_std()?;
462
463 let addr = listener.local_addr()?;
464
465 if !test {
466 eprintln!(
467 "Listening on {}://{addr}",
468 match config {
469 SpawnConfig::Https(_) => "https",
470 _ => "http",
471 }
472 );
473 }
474
475 if let Some(tx) = port_tx {
476 tx.send(addr.port()).unwrap();
477 }
478
479 match config {
480 SpawnConfig::Https(acceptor) => {
481 axum_server::from_tcp(listener)?
482 .handle(handle)
483 .acceptor(acceptor)
484 .serve(router.into_make_service())
485 .await
486 }
487 SpawnConfig::Redirect(destination) => {
488 axum_server::from_tcp(listener)?
489 .handle(handle)
490 .serve(
491 Router::new()
492 .fallback(Self::redirect_http_to_https)
493 .layer(Extension(destination))
494 .into_make_service(),
495 )
496 .await
497 }
498 SpawnConfig::Http => {
499 axum_server::from_tcp(listener)?
500 .handle(handle)
501 .serve(router.into_make_service())
502 .await
503 }
504 }
505 }))
506 }
507
508 fn acme_cache(acme_cache: Option<&PathBuf>, settings: &Settings) -> PathBuf {
509 match acme_cache {
510 Some(acme_cache) => acme_cache.clone(),
511 None => settings.data_dir().join("acme-cache"),
512 }
513 }
514
515 fn acme_domains(&self) -> Result<Vec<String>> {
516 if !self.acme_domain.is_empty() {
517 Ok(self.acme_domain.clone())
518 } else {
519 Ok(vec![
520 System::host_name().ok_or(anyhow!("no hostname found"))?,
521 ])
522 }
523 }
524
525 fn http_port(&self) -> Option<u16> {
526 if self.http || self.http_port.is_some() || (self.https_port.is_none() && !self.https) {
527 Some(self.http_port.unwrap_or(80))
528 } else {
529 None
530 }
531 }
532
533 fn https_port(&self) -> Option<u16> {
534 if self.https || self.https_port.is_some() {
535 Some(self.https_port.unwrap_or(443))
536 } else {
537 None
538 }
539 }
540
541 fn acceptor(&self, settings: &Settings) -> Result<AxumAcceptor> {
542 static RUSTLS_PROVIDER_INSTALLED: LazyLock<bool> = LazyLock::new(|| {
543 rustls::crypto::ring::default_provider()
544 .install_default()
545 .is_ok()
546 });
547
548 let config = AcmeConfig::new(self.acme_domains()?)
549 .contact(&self.acme_contact)
550 .cache_option(Some(DirCache::new(Self::acme_cache(
551 self.acme_cache.as_ref(),
552 settings,
553 ))))
554 .directory(if cfg!(test) {
555 LETS_ENCRYPT_STAGING_DIRECTORY
556 } else {
557 LETS_ENCRYPT_PRODUCTION_DIRECTORY
558 });
559
560 let mut state = config.state();
561
562 ensure! {
563 *RUSTLS_PROVIDER_INSTALLED,
564 "failed to install rustls ring crypto provider",
565 }
566
567 let mut server_config = rustls::ServerConfig::builder()
568 .with_no_client_auth()
569 .with_cert_resolver(state.resolver());
570
571 server_config.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
572
573 let acceptor = state.axum_acceptor(Arc::new(server_config));
574
575 tokio::spawn(async move {
576 while let Some(result) = state.next().await {
577 match result {
578 Ok(ok) => log::info!("ACME event: {ok:?}"),
579 Err(err) => log::error!("ACME error: {err:?}"),
580 }
581 }
582 });
583
584 Ok(acceptor)
585 }
586
587 async fn proxy_layer(
588 server_config: Extension<Arc<ServerConfig>>,
589 request: http::Request<axum::body::Body>,
590 next: axum::middleware::Next,
591 ) -> ServerResult {
592 let path = request.uri().path().to_owned();
593
594 let response = next.run(request).await;
595
596 if let Some(proxy) = &server_config.proxy {
597 if response.status() == StatusCode::NOT_FOUND {
598 return task::block_in_place(|| Server::proxy(proxy, &path));
599 }
600
601 if SAT_AT_INDEX_PATH.is_match(&path) {
604 let (parts, body) = response.into_parts();
605
606 let bytes = axum::body::to_bytes(body, usize::MAX)
607 .await
608 .map_err(|err| anyhow!(err))?;
609
610 if let Ok(api::SatInscription { id: None }) =
611 serde_json::from_slice::<api::SatInscription>(&bytes)
612 {
613 return task::block_in_place(|| Server::proxy(proxy, &path));
614 }
615
616 return Ok(Response::from_parts(parts, axum::body::Body::from(bytes)));
617 }
618 }
619
620 Ok(response)
621 }
622
623 fn index_height(index: &Index) -> ServerResult<Height> {
624 index.block_height()?.ok_or_not_found(|| "genesis block")
625 }
626
627 async fn clock(Extension(index): Extension<Arc<Index>>) -> ServerResult {
628 task::block_in_place(|| {
629 Ok(
630 (
631 [(
632 header::CONTENT_SECURITY_POLICY,
633 HeaderValue::from_static("default-src 'unsafe-inline'"),
634 )],
635 ClockSvg::new(Self::index_height(&index)?),
636 )
637 .into_response(),
638 )
639 })
640 }
641
642 async fn fallback(Extension(index): Extension<Arc<Index>>, uri: Uri) -> ServerResult<Response> {
643 task::block_in_place(|| {
644 let path = urlencoding::decode(uri.path().trim_matches('/'))
645 .map_err(|err| ServerError::BadRequest(err.to_string()))?;
646
647 let prefix = if re::INSCRIPTION_ID.is_match(&path) || re::INSCRIPTION_NUMBER.is_match(&path) {
648 "inscription"
649 } else if re::RUNE_ID.is_match(&path) || re::SPACED_RUNE.is_match(&path) {
650 "rune"
651 } else if re::OUTPOINT.is_match(&path) {
652 "output"
653 } else if re::SATPOINT.is_match(&path) {
654 "satpoint"
655 } else if re::HASH.is_match(&path) {
656 if index.block_header(path.parse().unwrap())?.is_some() {
657 "block"
658 } else {
659 "tx"
660 }
661 } else if re::ADDRESS.is_match(&path) {
662 "address"
663 } else {
664 return Ok(StatusCode::NOT_FOUND.into_response());
665 };
666
667 Ok(Redirect::to(&format!("/{prefix}/{path}")).into_response())
668 })
669 }
670
671 async fn satscard(
672 Extension(settings): Extension<Arc<Settings>>,
673 Extension(server_config): Extension<Arc<ServerConfig>>,
674 Extension(index): Extension<Arc<Index>>,
675 uri: Uri,
676 ) -> ServerResult<Response> {
677 #[derive(Debug, Deserialize)]
678 struct Form {
679 url: DeserializeFromStr<Url>,
680 }
681
682 if let Ok(form) = Query::<Form>::try_from_uri(&uri) {
683 return if let Some(fragment) = form.url.0.fragment() {
684 Ok(Redirect::to(&format!("/satscard?{fragment}")).into_response())
685 } else if let Some(query) = form.url.0.query() {
686 Ok(Redirect::to(&format!("/satscard?{query}")).into_response())
687 } else {
688 Err(ServerError::BadRequest(
689 "satscard URL missing fragment".into(),
690 ))
691 };
692 }
693
694 let satscard = if let Some(query) = uri.query().filter(|query| !query.is_empty()) {
695 let satscard = Satscard::from_query_parameters(settings.chain(), query).map_err(|err| {
696 ServerError::BadRequest(format!("invalid satscard query parameters: {err}"))
697 })?;
698
699 let address_info = Self::address_info(&index, &satscard.address)?.map(
700 |api::AddressInfo {
701 outputs,
702 inscriptions,
703 sat_balance,
704 runes_balances,
705 }| AddressHtml {
706 address: satscard.address.clone(),
707 header: false,
708 inscriptions,
709 outputs,
710 runes_balances,
711 sat_balance,
712 },
713 );
714
715 Some((satscard, address_info))
716 } else {
717 None
718 };
719
720 Ok(
721 SatscardHtml { satscard }
722 .page(server_config)
723 .into_response(),
724 )
725 }
726
727 async fn sat(
728 Extension(server_config): Extension<Arc<ServerConfig>>,
729 Extension(index): Extension<Arc<Index>>,
730 Path(DeserializeFromStr(sat)): Path<DeserializeFromStr<Sat>>,
731 AcceptJson(accept_json): AcceptJson,
732 ) -> ServerResult {
733 task::block_in_place(|| {
734 let inscriptions = index.get_inscription_ids_by_sat(sat)?;
735 let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| {
736 inscriptions.first().and_then(|&first_inscription_id| {
737 index
738 .get_inscription_satpoint_by_id(first_inscription_id)
739 .ok()
740 .flatten()
741 })
742 });
743 let blocktime = index.block_time(sat.height())?;
744
745 let charms = sat.charms();
746
747 let address = if let Some(satpoint) = satpoint {
748 if satpoint.outpoint == unbound_outpoint() {
749 None
750 } else {
751 let tx = index
752 .get_transaction(satpoint.outpoint.txid)?
753 .context("could not get transaction for sat")?;
754
755 let tx_out = tx
756 .output
757 .get::<usize>(satpoint.outpoint.vout.try_into().unwrap())
758 .context("could not get vout for sat")?;
759
760 server_config
761 .chain
762 .address_from_script(&tx_out.script_pubkey)
763 .ok()
764 }
765 } else {
766 None
767 };
768
769 Ok(if accept_json {
770 Json(api::Sat {
771 address: address.map(|address| address.to_string()),
772 block: sat.height().0,
773 charms: Charm::charms(charms),
774 cycle: sat.cycle(),
775 decimal: sat.decimal().to_string(),
776 degree: sat.degree().to_string(),
777 epoch: sat.epoch().0,
778 inscriptions,
779 name: sat.name(),
780 number: sat.0,
781 offset: sat.third(),
782 percentile: sat.percentile(),
783 period: sat.period(),
784 rarity: sat.rarity(),
785 satpoint,
786 timestamp: blocktime.timestamp().timestamp(),
787 })
788 .into_response()
789 } else {
790 SatHtml {
791 address,
792 blocktime,
793 inscriptions,
794 sat,
795 satpoint,
796 }
797 .page(server_config)
798 .into_response()
799 })
800 })
801 }
802
803 async fn ordinal(Path(sat): Path<String>) -> Redirect {
804 Redirect::to(&format!("/sat/{sat}"))
805 }
806
807 async fn output(
808 Extension(server_config): Extension<Arc<ServerConfig>>,
809 Extension(index): Extension<Arc<Index>>,
810 Path(outpoint): Path<OutPoint>,
811 AcceptJson(accept_json): AcceptJson,
812 ) -> ServerResult {
813 task::block_in_place(|| {
814 let (output_info, txout) = index
815 .get_output_info(outpoint)?
816 .ok_or_not_found(|| format!("output {outpoint}"))?;
817
818 Ok(if accept_json {
819 Json(output_info).into_response()
820 } else {
821 OutputHtml {
822 chain: server_config.chain,
823 confirmations: output_info.confirmations,
824 inscriptions: output_info.inscriptions,
825 outpoint,
826 output: txout,
827 runes: output_info.runes,
828 sat_ranges: output_info.sat_ranges,
829 spent: output_info.spent,
830 }
831 .page(server_config)
832 .into_response()
833 })
834 })
835 }
836
837 async fn satpoint(
838 Extension(index): Extension<Arc<Index>>,
839 Path(satpoint): Path<SatPoint>,
840 ) -> ServerResult<Redirect> {
841 task::block_in_place(|| {
842 let (output_info, _) = index
843 .get_output_info(satpoint.outpoint)?
844 .ok_or_not_found(|| format!("satpoint {satpoint}"))?;
845
846 let Some(ranges) = output_info.sat_ranges else {
847 return Err(ServerError::NotFound("sat index required".into()));
848 };
849
850 let mut total = 0;
851 for (start, end) in ranges {
852 let size = end - start;
853 if satpoint.offset < total + size {
854 let sat = start + satpoint.offset - total;
855
856 return Ok(Redirect::to(&format!("/sat/{sat}")));
857 }
858 total += size;
859 }
860
861 Err(ServerError::NotFound(format!(
862 "satpoint {satpoint} not found"
863 )))
864 })
865 }
866
867 async fn outputs(
868 Extension(index): Extension<Arc<Index>>,
869 AcceptJson(accept_json): AcceptJson,
870 Json(outputs): Json<Vec<OutPoint>>,
871 ) -> ServerResult {
872 task::block_in_place(|| {
873 Ok(if accept_json {
874 let mut response = Vec::new();
875 for outpoint in outputs {
876 let (output_info, _) = index
877 .get_output_info(outpoint)?
878 .ok_or_not_found(|| format!("output {outpoint}"))?;
879
880 response.push(output_info);
881 }
882 Json(response).into_response()
883 } else {
884 StatusCode::NOT_FOUND.into_response()
885 })
886 })
887 }
888
889 async fn outputs_address(
890 Extension(server_config): Extension<Arc<ServerConfig>>,
891 Extension(index): Extension<Arc<Index>>,
892 AcceptJson(accept_json): AcceptJson,
893 Path(address): Path<Address<NetworkUnchecked>>,
894 Query(query): Query<OutputsQuery>,
895 ) -> ServerResult {
896 task::block_in_place(|| {
897 if !index.has_address_index() {
898 return Err(ServerError::NotFound(
899 "this server has no address index".to_string(),
900 ));
901 }
902
903 if !accept_json {
904 return Ok(StatusCode::NOT_FOUND.into_response());
905 }
906
907 let output_type = query.ty.unwrap_or_default();
908
909 if output_type != OutputType::Any {
910 if !index.has_rune_index() {
911 return Err(ServerError::BadRequest(
912 "this server has no runes index".to_string(),
913 ));
914 }
915
916 if !index.has_inscription_index() {
917 return Err(ServerError::BadRequest(
918 "this server has no inscriptions index".to_string(),
919 ));
920 }
921 }
922
923 let address = address
924 .require_network(server_config.chain.network())
925 .map_err(|err| ServerError::BadRequest(err.to_string()))?;
926
927 let outputs = index.get_address_info(&address)?;
928
929 let mut response = Vec::new();
930 for output in outputs.into_iter() {
931 let include = match output_type {
932 OutputType::Any => true,
933 OutputType::Cardinal => {
934 index
935 .get_inscriptions_on_output_with_satpoints(output)?
936 .unwrap_or_default()
937 .is_empty()
938 && index
939 .get_rune_balances_for_output(output)?
940 .unwrap_or_default()
941 .is_empty()
942 }
943 OutputType::Inscribed => !index
944 .get_inscriptions_on_output_with_satpoints(output)?
945 .unwrap_or_default()
946 .is_empty(),
947 OutputType::Runic => !index
948 .get_rune_balances_for_output(output)?
949 .unwrap_or_default()
950 .is_empty(),
951 };
952
953 if include {
954 let (output_info, _) = index
955 .get_output_info(output)?
956 .ok_or_not_found(|| format!("output {output}"))?;
957
958 response.push(output_info);
959 }
960 }
961
962 Ok(Json(response).into_response())
963 })
964 }
965
966 async fn rare_txt(Extension(index): Extension<Arc<Index>>) -> ServerResult<RareTxt> {
967 task::block_in_place(|| Ok(RareTxt(index.rare_sat_satpoints()?)))
968 }
969
970 async fn rune(
971 Extension(server_config): Extension<Arc<ServerConfig>>,
972 Extension(index): Extension<Arc<Index>>,
973 Path(DeserializeFromStr(rune_query)): Path<DeserializeFromStr<query::Rune>>,
974 AcceptJson(accept_json): AcceptJson,
975 ) -> ServerResult {
976 task::block_in_place(|| {
977 if !index.has_rune_index() {
978 return Err(ServerError::NotFound(
979 "this server has no rune index".to_string(),
980 ));
981 }
982
983 let rune = match rune_query {
984 query::Rune::Spaced(spaced_rune) => spaced_rune.rune,
985 query::Rune::Id(rune_id) => index
986 .get_rune_by_id(rune_id)?
987 .ok_or_not_found(|| format!("rune {rune_id}"))?,
988 query::Rune::Number(number) => index
989 .get_rune_by_number(usize::try_from(number).unwrap())?
990 .ok_or_not_found(|| format!("rune number {number}"))?,
991 };
992
993 let Some((id, entry, parent)) = index.rune(rune)? else {
994 return Ok(if accept_json {
995 StatusCode::NOT_FOUND.into_response()
996 } else {
997 let unlock = if let Some(height) = rune.unlock_height(server_config.chain.network()) {
998 Some((height, index.block_time(height)?))
999 } else {
1000 None
1001 };
1002
1003 (
1004 StatusCode::NOT_FOUND,
1005 RuneNotFoundHtml { rune, unlock }.page(server_config),
1006 )
1007 .into_response()
1008 });
1009 };
1010
1011 let block_height = index.block_height()?.unwrap_or(Height(0));
1012
1013 let mintable = entry.mintable((block_height.n() + 1).into()).is_ok();
1014
1015 Ok(if accept_json {
1016 Json(api::Rune {
1017 entry,
1018 id,
1019 mintable,
1020 parent,
1021 })
1022 .into_response()
1023 } else {
1024 RuneHtml {
1025 entry,
1026 id,
1027 mintable,
1028 parent,
1029 }
1030 .page(server_config)
1031 .into_response()
1032 })
1033 })
1034 }
1035
1036 async fn runes(
1037 Extension(server_config): Extension<Arc<ServerConfig>>,
1038 Extension(index): Extension<Arc<Index>>,
1039 accept_json: AcceptJson,
1040 ) -> ServerResult<Response> {
1041 Self::runes_paginated(
1042 Extension(server_config),
1043 Extension(index),
1044 Path(0),
1045 accept_json,
1046 )
1047 .await
1048 }
1049
1050 async fn runes_paginated(
1051 Extension(server_config): Extension<Arc<ServerConfig>>,
1052 Extension(index): Extension<Arc<Index>>,
1053 Path(page_index): Path<usize>,
1054 AcceptJson(accept_json): AcceptJson,
1055 ) -> ServerResult {
1056 task::block_in_place(|| {
1057 let (entries, more) = index.runes_paginated(50, page_index)?;
1058
1059 let prev = page_index.checked_sub(1);
1060
1061 let next = more.then_some(page_index + 1);
1062
1063 Ok(if accept_json {
1064 Json(RunesHtml {
1065 entries,
1066 more,
1067 prev,
1068 next,
1069 })
1070 .into_response()
1071 } else {
1072 RunesHtml {
1073 entries,
1074 more,
1075 prev,
1076 next,
1077 }
1078 .page(server_config)
1079 .into_response()
1080 })
1081 })
1082 }
1083
1084 async fn home(
1085 Extension(server_config): Extension<Arc<ServerConfig>>,
1086 Extension(index): Extension<Arc<Index>>,
1087 ) -> ServerResult<PageHtml<HomeHtml>> {
1088 task::block_in_place(|| {
1089 Ok(
1090 HomeHtml {
1091 inscriptions: index.get_home_inscriptions()?,
1092 }
1093 .page(server_config),
1094 )
1095 })
1096 }
1097
1098 async fn blocks(
1099 Extension(server_config): Extension<Arc<ServerConfig>>,
1100 Extension(index): Extension<Arc<Index>>,
1101 AcceptJson(accept_json): AcceptJson,
1102 ) -> ServerResult {
1103 task::block_in_place(|| {
1104 let blocks = index.blocks(100)?;
1105 let mut featured_blocks = BTreeMap::new();
1106 for (height, hash) in blocks.iter().take(5) {
1107 let (inscriptions, _total_num) =
1108 index.get_highest_paying_inscriptions_in_block(*height, 8)?;
1109
1110 featured_blocks.insert(*hash, inscriptions);
1111 }
1112
1113 Ok(if accept_json {
1114 Json(api::Blocks::new(blocks, featured_blocks)).into_response()
1115 } else {
1116 BlocksHtml::new(blocks, featured_blocks)
1117 .page(server_config)
1118 .into_response()
1119 })
1120 })
1121 }
1122
1123 async fn install_script() -> Redirect {
1124 Redirect::to("https://raw.githubusercontent.com/ordinals/ord/master/install.sh")
1125 }
1126
1127 async fn offer(
1128 Extension(server_config): Extension<Arc<ServerConfig>>,
1129 Extension(index): Extension<Arc<Index>>,
1130 offer: String,
1131 ) -> ServerResult {
1132 if !server_config.accept_offers {
1133 return Err(ServerError::NotFound(
1134 "this server does not accept offers".into(),
1135 ));
1136 }
1137
1138 task::block_in_place(|| {
1139 let offer = base64_decode(&offer)
1140 .map_err(|err| ServerError::BadRequest(format!("failed to base64 decode PSBT: {err}")))?;
1141
1142 let offer = Psbt::deserialize(&offer)
1143 .map_err(|err| ServerError::BadRequest(format!("invalid offer PSBT: {err}")))?;
1144
1145 index.insert_offer(offer).map_err(ServerError::Internal)?;
1146
1147 Ok("".into_response())
1148 })
1149 }
1150
1151 async fn offers(
1152 Extension(index): Extension<Arc<Index>>,
1153 AcceptJson(accept_json): AcceptJson,
1154 ) -> ServerResult {
1155 if !accept_json {
1156 return Ok(StatusCode::NOT_FOUND.into_response());
1157 }
1158
1159 task::block_in_place(|| {
1160 Ok(
1161 Json(api::Offers {
1162 offers: index
1163 .get_offers()?
1164 .into_iter()
1165 .map(|offer| base64_encode(&offer))
1166 .collect(),
1167 })
1168 .into_response(),
1169 )
1170 })
1171 }
1172
1173 async fn address(
1174 Extension(server_config): Extension<Arc<ServerConfig>>,
1175 Extension(index): Extension<Arc<Index>>,
1176 Path(address): Path<Address<NetworkUnchecked>>,
1177 AcceptJson(accept_json): AcceptJson,
1178 ) -> ServerResult {
1179 task::block_in_place(|| {
1180 let address = address
1181 .require_network(server_config.chain.network())
1182 .map_err(|err| ServerError::BadRequest(err.to_string()))?;
1183
1184 let Some(info) = Self::address_info(&index, &address)? else {
1185 return Err(ServerError::NotFound(
1186 "this server has no address index".to_string(),
1187 ));
1188 };
1189
1190 Ok(if accept_json {
1191 Json(info).into_response()
1192 } else {
1193 let api::AddressInfo {
1194 sat_balance,
1195 outputs,
1196 inscriptions,
1197 runes_balances,
1198 } = info;
1199
1200 AddressHtml {
1201 address,
1202 header: true,
1203 inscriptions,
1204 outputs,
1205 runes_balances,
1206 sat_balance,
1207 }
1208 .page(server_config)
1209 .into_response()
1210 })
1211 })
1212 }
1213
1214 fn address_info(index: &Index, address: &Address) -> ServerResult<Option<api::AddressInfo>> {
1215 if !index.has_address_index() {
1216 return Ok(None);
1217 }
1218
1219 let mut outputs = index.get_address_info(address)?;
1220
1221 outputs.sort();
1222
1223 let sat_balance = index.get_sat_balances_for_outputs(&outputs)?;
1224
1225 let inscriptions = index.get_inscriptions_for_outputs(&outputs)?;
1226
1227 let runes_balances = index.get_aggregated_rune_balances_for_outputs(&outputs)?;
1228
1229 Ok(Some(api::AddressInfo {
1230 sat_balance,
1231 outputs,
1232 inscriptions,
1233 runes_balances,
1234 }))
1235 }
1236
1237 async fn block(
1238 Extension(server_config): Extension<Arc<ServerConfig>>,
1239 Extension(index): Extension<Arc<Index>>,
1240 Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Block>>,
1241 AcceptJson(accept_json): AcceptJson,
1242 ) -> ServerResult {
1243 task::block_in_place(|| {
1244 let (block, height) = match query {
1245 query::Block::Height(height) => {
1246 let block = index
1247 .get_block_by_height(height)?
1248 .ok_or_not_found(|| format!("block {height}"))?;
1249
1250 (block, height)
1251 }
1252 query::Block::Hash(hash) => {
1253 let info = index
1254 .block_header_info(hash)?
1255 .ok_or_not_found(|| format!("block {hash}"))?;
1256
1257 let block = index
1258 .get_block_by_hash(hash)?
1259 .ok_or_not_found(|| format!("block {hash}"))?;
1260
1261 (block, u32::try_from(info.height).unwrap())
1262 }
1263 };
1264
1265 let runes = index.get_runes_in_block(u64::from(height))?;
1266 Ok(if accept_json {
1267 let inscriptions = index.get_inscriptions_in_block(height)?;
1268 Json(api::Block::new(
1269 block,
1270 Height(height),
1271 Self::index_height(&index)?,
1272 inscriptions,
1273 runes,
1274 ))
1275 .into_response()
1276 } else {
1277 let (featured_inscriptions, total_num) =
1278 index.get_highest_paying_inscriptions_in_block(height, 8)?;
1279 BlockHtml::new(
1280 block,
1281 Height(height),
1282 Self::index_height(&index)?,
1283 total_num,
1284 featured_inscriptions,
1285 runes,
1286 )
1287 .page(server_config)
1288 .into_response()
1289 })
1290 })
1291 }
1292
1293 async fn transaction(
1294 Extension(server_config): Extension<Arc<ServerConfig>>,
1295 Extension(index): Extension<Arc<Index>>,
1296 Path(txid): Path<Txid>,
1297 AcceptJson(accept_json): AcceptJson,
1298 ) -> ServerResult {
1299 task::block_in_place(|| {
1300 let transaction = index
1301 .get_transaction(txid)?
1302 .ok_or_not_found(|| format!("transaction {txid}"))?;
1303
1304 let inscription_count = index.inscription_count(txid)?;
1305
1306 Ok(if accept_json {
1307 Json(api::Transaction {
1308 chain: server_config.chain,
1309 etching: index.get_etching(txid)?,
1310 inscription_count,
1311 transaction,
1312 txid,
1313 })
1314 .into_response()
1315 } else {
1316 TransactionHtml {
1317 chain: server_config.chain,
1318 etching: index.get_etching(txid)?,
1319 inscription_count,
1320 transaction,
1321 txid,
1322 }
1323 .page(server_config)
1324 .into_response()
1325 })
1326 })
1327 }
1328
1329 async fn decode(
1330 Extension(index): Extension<Arc<Index>>,
1331 Path(txid): Path<Txid>,
1332 AcceptJson(accept_json): AcceptJson,
1333 ) -> ServerResult {
1334 task::block_in_place(|| {
1335 let transaction = index
1336 .get_transaction(txid)?
1337 .ok_or_not_found(|| format!("transaction {txid}"))?;
1338
1339 let inscriptions = ParsedEnvelope::from_transaction(&transaction);
1340 let runestone = Runestone::decipher(&transaction);
1341
1342 Ok(if accept_json {
1343 Json(api::Decode {
1344 inscriptions,
1345 runestone,
1346 })
1347 .into_response()
1348 } else {
1349 StatusCode::NOT_FOUND.into_response()
1350 })
1351 })
1352 }
1353
1354 async fn update(
1355 Extension(settings): Extension<Arc<Settings>>,
1356 Extension(index): Extension<Arc<Index>>,
1357 ) -> ServerResult {
1358 task::block_in_place(|| {
1359 if settings.integration_test() {
1360 index.update()?;
1361 Ok(index.block_count()?.to_string().into_response())
1362 } else {
1363 Ok(StatusCode::NOT_FOUND.into_response())
1364 }
1365 })
1366 }
1367
1368 async fn status(
1369 Extension(server_config): Extension<Arc<ServerConfig>>,
1370 Extension(index): Extension<Arc<Index>>,
1371 AcceptJson(accept_json): AcceptJson,
1372 ) -> ServerResult {
1373 task::block_in_place(|| {
1374 Ok(if accept_json {
1375 Json(index.status(server_config.json_api_enabled)?).into_response()
1376 } else {
1377 index
1378 .status(server_config.json_api_enabled)?
1379 .page(server_config)
1380 .into_response()
1381 })
1382 })
1383 }
1384
1385 async fn search_by_query(
1386 Extension(index): Extension<Arc<Index>>,
1387 Query(search): Query<Search>,
1388 ) -> ServerResult<Redirect> {
1389 Self::search(index, search.query).await
1390 }
1391
1392 async fn search_by_path(
1393 Extension(index): Extension<Arc<Index>>,
1394 Path(search): Path<Search>,
1395 ) -> ServerResult<Redirect> {
1396 Self::search(index, search.query).await
1397 }
1398
1399 async fn search(index: Arc<Index>, query: String) -> ServerResult<Redirect> {
1400 task::block_in_place(|| {
1401 let query = query.trim();
1402
1403 if re::HASH.is_match(query) {
1404 if index.block_header(query.parse().unwrap())?.is_some() {
1405 Ok(Redirect::to(&format!("/block/{query}")))
1406 } else {
1407 Ok(Redirect::to(&format!("/tx/{query}")))
1408 }
1409 } else if re::OUTPOINT.is_match(query) {
1410 Ok(Redirect::to(&format!("/output/{query}")))
1411 } else if re::INSCRIPTION_ID.is_match(query) || re::INSCRIPTION_NUMBER.is_match(query) {
1412 Ok(Redirect::to(&format!("/inscription/{query}")))
1413 } else if let Some(captures) = re::COINKITE_SATSCARD_URL.captures(query) {
1414 Ok(Redirect::to(&format!(
1415 "/satscard?{}",
1416 &captures["parameters"]
1417 )))
1418 } else if let Some(captures) = re::ORDINALS_SATSCARD_URL.captures(query) {
1419 Ok(Redirect::to(&format!("/satscard?{}", &captures["query"])))
1420 } else if re::SPACED_RUNE.is_match(query) {
1421 Ok(Redirect::to(&format!("/rune/{query}")))
1422 } else if re::RUNE_ID.is_match(query) {
1423 let id = query
1424 .parse::<RuneId>()
1425 .map_err(|err| ServerError::BadRequest(err.to_string()))?;
1426
1427 let rune = index.get_rune_by_id(id)?.ok_or_not_found(|| "rune ID")?;
1428
1429 Ok(Redirect::to(&format!("/rune/{rune}")))
1430 } else if re::ADDRESS.is_match(query) {
1431 Ok(Redirect::to(&format!("/address/{query}")))
1432 } else if re::SATPOINT.is_match(query) {
1433 Ok(Redirect::to(&format!("/satpoint/{query}")))
1434 } else {
1435 Ok(Redirect::to(&format!("/sat/{query}")))
1436 }
1437 })
1438 }
1439
1440 async fn favicon() -> ServerResult {
1441 Ok(
1442 Self::static_asset(Path("/favicon.png".to_string()))
1443 .await
1444 .into_response(),
1445 )
1446 }
1447
1448 async fn feed(
1449 Extension(server_config): Extension<Arc<ServerConfig>>,
1450 Extension(index): Extension<Arc<Index>>,
1451 ) -> ServerResult {
1452 task::block_in_place(|| {
1453 let mut builder = rss::ChannelBuilder::default();
1454
1455 let chain = server_config.chain;
1456 match chain {
1457 Chain::Mainnet => builder.title("Inscriptions".to_string()),
1458 _ => builder.title(format!("Inscriptions – {chain:?}")),
1459 };
1460
1461 builder.generator(Some("ord".to_string()));
1462
1463 for (number, id) in index.get_feed_inscriptions(300)? {
1464 builder.item(
1465 rss::ItemBuilder::default()
1466 .title(Some(format!("Inscription {number}")))
1467 .link(Some(format!("/inscription/{id}")))
1468 .guid(Some(rss::Guid {
1469 value: format!("/inscription/{id}"),
1470 permalink: true,
1471 }))
1472 .build(),
1473 );
1474 }
1475
1476 Ok(
1477 (
1478 [
1479 (header::CONTENT_TYPE, "application/rss+xml"),
1480 (
1481 header::CONTENT_SECURITY_POLICY,
1482 "default-src 'unsafe-inline'",
1483 ),
1484 ],
1485 builder.build().to_string(),
1486 )
1487 .into_response(),
1488 )
1489 })
1490 }
1491
1492 async fn static_asset(Path(path): Path<String>) -> ServerResult {
1493 let content = StaticAssets::get(if let Some(stripped) = path.strip_prefix('/') {
1494 stripped
1495 } else {
1496 &path
1497 })
1498 .ok_or_not_found(|| format!("asset {path}"))?;
1499
1500 let mime = mime_guess::from_path(path).first_or_octet_stream();
1501
1502 Ok(
1503 Response::builder()
1504 .header(header::CONTENT_TYPE, mime.as_ref())
1505 .body(content.data.into())
1506 .unwrap(),
1507 )
1508 }
1509
1510 async fn block_count(Extension(index): Extension<Arc<Index>>) -> ServerResult<String> {
1511 task::block_in_place(|| Ok(index.block_count()?.to_string()))
1512 }
1513
1514 async fn input(
1515 Extension(server_config): Extension<Arc<ServerConfig>>,
1516 Extension(index): Extension<Arc<Index>>,
1517 Path(path): Path<(u32, usize, usize)>,
1518 ) -> ServerResult<PageHtml<InputHtml>> {
1519 task::block_in_place(|| {
1520 let not_found = || format!("input /{}/{}/{}", path.0, path.1, path.2);
1521
1522 let block = index
1523 .get_block_by_height(path.0)?
1524 .ok_or_not_found(not_found)?;
1525
1526 let transaction = block
1527 .txdata
1528 .into_iter()
1529 .nth(path.1)
1530 .ok_or_not_found(not_found)?;
1531
1532 let input = transaction
1533 .input
1534 .into_iter()
1535 .nth(path.2)
1536 .ok_or_not_found(not_found)?;
1537
1538 Ok(InputHtml { path, input }.page(server_config))
1539 })
1540 }
1541
1542 async fn faq() -> Redirect {
1543 Redirect::to("https://docs.ordinals.com/faq")
1544 }
1545
1546 async fn bounties() -> Redirect {
1547 Redirect::to("https://docs.ordinals.com/bounties")
1548 }
1549
1550 async fn preview(
1551 Extension(index): Extension<Arc<Index>>,
1552 Extension(settings): Extension<Arc<Settings>>,
1553 Extension(server_config): Extension<Arc<ServerConfig>>,
1554 Path(inscription_id): Path<InscriptionId>,
1555 accept_encoding: AcceptEncoding,
1556 ) -> ServerResult {
1557 task::block_in_place(|| {
1558 if settings.is_hidden(inscription_id) {
1559 return Ok(PreviewUnknownHtml.into_response());
1560 }
1561
1562 let mut inscription = index
1563 .get_inscription_by_id(inscription_id)?
1564 .ok_or_not_found(|| format!("inscription {inscription_id}"))?;
1565
1566 let inscription_number = index
1567 .get_inscription_entry(inscription_id)?
1568 .ok_or_not_found(|| format!("inscription {inscription_id}"))?
1569 .inscription_number;
1570
1571 if let Some(delegate) = inscription.delegate() {
1572 inscription = index
1573 .get_inscription_by_id(delegate)?
1574 .ok_or_not_found(|| format!("delegate {inscription_id}"))?
1575 }
1576
1577 let media = inscription.media();
1578
1579 if let Media::Iframe = media {
1580 return Ok(
1581 r::content_response(inscription, accept_encoding, &server_config, true)?
1582 .ok_or_not_found(|| format!("inscription {inscription_id} content"))?
1583 .into_response(),
1584 );
1585 }
1586
1587 let content_security_policy = server_config.preview_content_security_policy(media)?;
1588
1589 match media {
1590 Media::Audio => Ok(
1591 (
1592 content_security_policy,
1593 PreviewAudioHtml {
1594 inscription_id,
1595 inscription_number,
1596 },
1597 )
1598 .into_response(),
1599 ),
1600 Media::Code(language) => Ok(
1601 (
1602 content_security_policy,
1603 PreviewCodeHtml {
1604 inscription_id,
1605 language,
1606 inscription_number,
1607 },
1608 )
1609 .into_response(),
1610 ),
1611 Media::Font => Ok(
1612 (
1613 content_security_policy,
1614 PreviewFontHtml {
1615 inscription_id,
1616 inscription_number,
1617 },
1618 )
1619 .into_response(),
1620 ),
1621 Media::Iframe => unreachable!(),
1622 Media::Image(image_rendering) => Ok(
1623 (
1624 content_security_policy,
1625 PreviewImageHtml {
1626 image_rendering,
1627 inscription_id,
1628 inscription_number,
1629 },
1630 )
1631 .into_response(),
1632 ),
1633 Media::Markdown => Ok(
1634 (
1635 content_security_policy,
1636 PreviewMarkdownHtml {
1637 inscription_id,
1638 inscription_number,
1639 },
1640 )
1641 .into_response(),
1642 ),
1643 Media::Model => Ok(
1644 (
1645 content_security_policy,
1646 PreviewModelHtml {
1647 inscription_id,
1648 inscription_number,
1649 },
1650 )
1651 .into_response(),
1652 ),
1653 Media::Pdf => Ok(
1654 (
1655 content_security_policy,
1656 PreviewPdfHtml {
1657 inscription_id,
1658 inscription_number,
1659 },
1660 )
1661 .into_response(),
1662 ),
1663 Media::Text => Ok(
1664 (
1665 content_security_policy,
1666 PreviewTextHtml {
1667 inscription_id,
1668 inscription_number,
1669 },
1670 )
1671 .into_response(),
1672 ),
1673 Media::Unknown => Ok((content_security_policy, PreviewUnknownHtml).into_response()),
1674 Media::Video => Ok(
1675 (
1676 content_security_policy,
1677 PreviewVideoHtml {
1678 inscription_id,
1679 inscription_number,
1680 },
1681 )
1682 .into_response(),
1683 ),
1684 }
1685 })
1686 }
1687
1688 async fn item(
1689 Extension(server_config): Extension<Arc<ServerConfig>>,
1690 Extension(index): Extension<Arc<Index>>,
1691 Path((DeserializeFromStr(query), i)): Path<(DeserializeFromStr<query::Inscription>, usize)>,
1692 ) -> ServerResult<PageHtml<ItemHtml>> {
1693 task::block_in_place(|| {
1694 if let query::Inscription::Sat(_) = query
1695 && !index.has_sat_index()
1696 {
1697 return Err(ServerError::NotFound("sat index required".into()));
1698 }
1699
1700 let (info, _txout, inscription) = index
1701 .inscription_info(query, None)?
1702 .ok_or_not_found(|| format!("inscription {query}"))?;
1703
1704 let properties = inscription.properties();
1705
1706 let item = properties
1707 .gallery
1708 .get(i)
1709 .ok_or_not_found(|| format!("gallery {query} item {i}"))?
1710 .clone();
1711
1712 Ok(
1713 ItemHtml {
1714 gallery_id: info.id,
1715 gallery_number: info.number,
1716 i,
1717 item,
1718 }
1719 .page(server_config),
1720 )
1721 })
1722 }
1723
1724 async fn inscription(
1725 Extension(server_config): Extension<Arc<ServerConfig>>,
1726 Extension(index): Extension<Arc<Index>>,
1727 AcceptJson(accept_json): AcceptJson,
1728 Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Inscription>>,
1729 ) -> ServerResult {
1730 Self::inscription_inner(server_config, &index, accept_json, query, None).await
1731 }
1732
1733 async fn inscription_child(
1734 Extension(server_config): Extension<Arc<ServerConfig>>,
1735 Extension(index): Extension<Arc<Index>>,
1736 AcceptJson(accept_json): AcceptJson,
1737 Path((DeserializeFromStr(query), child)): Path<(DeserializeFromStr<query::Inscription>, usize)>,
1738 ) -> ServerResult {
1739 Self::inscription_inner(server_config, &index, accept_json, query, Some(child)).await
1740 }
1741
1742 async fn inscription_inner(
1743 server_config: Arc<ServerConfig>,
1744 index: &Index,
1745 accept_json: bool,
1746 query: query::Inscription,
1747 child: Option<usize>,
1748 ) -> ServerResult {
1749 task::block_in_place(|| {
1750 if let query::Inscription::Sat(_) = query
1751 && !index.has_sat_index()
1752 {
1753 return Err(ServerError::NotFound("sat index required".into()));
1754 }
1755
1756 let inscription_info = index.inscription_info(query, child)?;
1757
1758 Ok(if accept_json {
1759 let status_code = if inscription_info.is_none() {
1760 StatusCode::NOT_FOUND
1761 } else {
1762 StatusCode::OK
1763 };
1764
1765 (status_code, Json(inscription_info.map(|info| info.0))).into_response()
1766 } else {
1767 let (info, txout, inscription) =
1768 inscription_info.ok_or_not_found(|| format!("inscription {query}"))?;
1769
1770 let properties = inscription.properties();
1771
1772 InscriptionHtml {
1773 chain: server_config.chain,
1774 charms: Charm::Vindicated.unset(info.charms.iter().fold(0, |mut acc, charm| {
1775 charm.set(&mut acc);
1776 acc
1777 })),
1778 child_count: info.child_count,
1779 children: info.children,
1780 fee: info.fee,
1781 height: info.height,
1782 id: info.id,
1783 inscription,
1784 next: info.next,
1785 number: info.number,
1786 output: txout,
1787 parents: info.parents,
1788 previous: info.previous,
1789 properties,
1790 rune: info.rune,
1791 sat: info.sat,
1792 satpoint: info.satpoint,
1793 timestamp: Utc.timestamp_opt(info.timestamp, 0).unwrap(),
1794 }
1795 .page(server_config)
1796 .into_response()
1797 })
1798 })
1799 }
1800
1801 async fn inscriptions_json(
1802 Extension(index): Extension<Arc<Index>>,
1803 AcceptJson(accept_json): AcceptJson,
1804 Json(inscriptions): Json<Vec<InscriptionId>>,
1805 ) -> ServerResult {
1806 task::block_in_place(|| {
1807 Ok(if accept_json {
1808 let mut response = Vec::new();
1809 for inscription in inscriptions {
1810 let query = query::Inscription::Id(inscription);
1811 let (info, _, _) = index
1812 .inscription_info(query, None)?
1813 .ok_or_not_found(|| format!("inscription {query}"))?;
1814
1815 response.push(info);
1816 }
1817
1818 Json(response).into_response()
1819 } else {
1820 StatusCode::NOT_FOUND.into_response()
1821 })
1822 })
1823 }
1824
1825 async fn missing(
1826 Extension(index): Extension<Arc<Index>>,
1827 AcceptJson(accept_json): AcceptJson,
1828 Json(inscription_ids): Json<Vec<InscriptionId>>,
1829 ) -> ServerResult {
1830 task::block_in_place(|| {
1831 Ok(if accept_json {
1832 Json(index.missing_inscriptions(&inscription_ids)?).into_response()
1833 } else {
1834 StatusCode::NOT_FOUND.into_response()
1835 })
1836 })
1837 }
1838
1839 async fn collections(
1840 Extension(server_config): Extension<Arc<ServerConfig>>,
1841 Extension(index): Extension<Arc<Index>>,
1842 ) -> ServerResult {
1843 Self::collections_paginated(Extension(server_config), Extension(index), Path(0)).await
1844 }
1845
1846 async fn collections_paginated(
1847 Extension(server_config): Extension<Arc<ServerConfig>>,
1848 Extension(index): Extension<Arc<Index>>,
1849 Path(page_index): Path<usize>,
1850 ) -> ServerResult {
1851 task::block_in_place(|| {
1852 let (collections, more_collections) =
1853 index.get_collections_paginated(PAGE_SIZE, page_index)?;
1854
1855 let prev = page_index.checked_sub(1);
1856
1857 let next = more_collections.then_some(page_index + 1);
1858
1859 Ok(
1860 CollectionsHtml {
1861 inscriptions: collections,
1862 prev,
1863 next,
1864 }
1865 .page(server_config)
1866 .into_response(),
1867 )
1868 })
1869 }
1870
1871 async fn galleries(
1872 Extension(server_config): Extension<Arc<ServerConfig>>,
1873 Extension(index): Extension<Arc<Index>>,
1874 accept_json: AcceptJson,
1875 ) -> ServerResult {
1876 Self::galleries_paginated(
1877 Extension(server_config),
1878 Extension(index),
1879 Path(0),
1880 accept_json,
1881 )
1882 .await
1883 }
1884
1885 async fn galleries_paginated(
1886 Extension(server_config): Extension<Arc<ServerConfig>>,
1887 Extension(index): Extension<Arc<Index>>,
1888 Path(page_index): Path<u32>,
1889 AcceptJson(accept_json): AcceptJson,
1890 ) -> ServerResult {
1891 task::block_in_place(|| {
1892 let (galleries, more) = index.get_galleries_paginated(PAGE_SIZE, page_index.into_usize())?;
1893
1894 let prev = page_index.checked_sub(1);
1895
1896 let next = more.then_some(page_index + 1);
1897
1898 Ok(if accept_json {
1899 Json(api::Inscriptions {
1900 ids: galleries,
1901 page_index,
1902 more,
1903 })
1904 .into_response()
1905 } else {
1906 GalleriesHtml {
1907 inscriptions: galleries,
1908 prev,
1909 next,
1910 }
1911 .page(server_config)
1912 .into_response()
1913 })
1914 })
1915 }
1916
1917 async fn gallery(
1918 Extension(server_config): Extension<Arc<ServerConfig>>,
1919 Extension(index): Extension<Arc<Index>>,
1920 Path(inscription_id): Path<InscriptionId>,
1921 ) -> ServerResult<PageHtml<GalleryHtml>> {
1922 Self::gallery_paginated(
1923 Extension(server_config),
1924 Extension(index),
1925 Path((inscription_id, 0)),
1926 )
1927 .await
1928 }
1929
1930 async fn gallery_paginated(
1931 Extension(server_config): Extension<Arc<ServerConfig>>,
1932 Extension(index): Extension<Arc<Index>>,
1933 Path((id, page)): Path<(InscriptionId, usize)>,
1934 ) -> ServerResult<PageHtml<GalleryHtml>> {
1935 task::block_in_place(|| {
1936 let inscription = index
1937 .get_inscription_by_id(id)?
1938 .ok_or_not_found(|| format!("inscription {id}"))?;
1939
1940 let number = index
1941 .get_inscription_entry(id)?
1942 .ok_or_not_found(|| format!("inscription {id}"))?
1943 .inscription_number;
1944
1945 let properties = inscription.properties();
1946
1947 let mut items = properties
1948 .gallery
1949 .iter()
1950 .enumerate()
1951 .skip(page.saturating_mul(PAGE_SIZE))
1952 .take(PAGE_SIZE.saturating_add(1))
1953 .map(|(i, item)| (i, item.id()))
1954 .collect::<Vec<(usize, InscriptionId)>>();
1955
1956 let more = items.len() > PAGE_SIZE;
1957
1958 if more {
1959 items.pop();
1960 }
1961
1962 let prev_page = page.checked_sub(1);
1963 let next_page = more.then_some(page + 1);
1964
1965 Ok(
1966 GalleryHtml {
1967 id,
1968 number,
1969 items,
1970 prev_page,
1971 next_page,
1972 }
1973 .page(server_config),
1974 )
1975 })
1976 }
1977
1978 async fn children(
1979 Extension(server_config): Extension<Arc<ServerConfig>>,
1980 Extension(index): Extension<Arc<Index>>,
1981 Path(inscription_id): Path<InscriptionId>,
1982 AcceptJson(accept_json): AcceptJson,
1983 ) -> ServerResult {
1984 Self::children_paginated(
1985 Extension(server_config),
1986 Extension(index),
1987 Path((inscription_id, 0)),
1988 AcceptJson(accept_json),
1989 )
1990 .await
1991 }
1992
1993 async fn children_paginated(
1994 Extension(server_config): Extension<Arc<ServerConfig>>,
1995 Extension(index): Extension<Arc<Index>>,
1996 Path((parent, page)): Path<(InscriptionId, usize)>,
1997 AcceptJson(accept_json): AcceptJson,
1998 ) -> ServerResult {
1999 task::block_in_place(|| {
2000 let entry = index
2001 .get_inscription_entry(parent)?
2002 .ok_or_not_found(|| format!("inscription {parent}"))?;
2003
2004 let parent_number = entry.inscription_number;
2005
2006 let (children, more_children) =
2007 index.get_children_by_sequence_number_paginated(entry.sequence_number, PAGE_SIZE, page)?;
2008
2009 let prev_page = page.checked_sub(1);
2010
2011 let next_page = more_children.then_some(page + 1);
2012
2013 Ok(if accept_json {
2014 Json(api::Children {
2015 ids: children,
2016 more: more_children,
2017 page,
2018 })
2019 .into_response()
2020 } else {
2021 ChildrenHtml {
2022 parent,
2023 parent_number,
2024 children,
2025 prev_page,
2026 next_page,
2027 }
2028 .page(server_config)
2029 .into_response()
2030 })
2031 })
2032 }
2033
2034 async fn inscriptions(
2035 Extension(server_config): Extension<Arc<ServerConfig>>,
2036 Extension(index): Extension<Arc<Index>>,
2037 accept_json: AcceptJson,
2038 ) -> ServerResult {
2039 Self::inscriptions_paginated(
2040 Extension(server_config),
2041 Extension(index),
2042 Path(0),
2043 accept_json,
2044 )
2045 .await
2046 }
2047
2048 async fn inscriptions_paginated(
2049 Extension(server_config): Extension<Arc<ServerConfig>>,
2050 Extension(index): Extension<Arc<Index>>,
2051 Path(page_index): Path<u32>,
2052 AcceptJson(accept_json): AcceptJson,
2053 ) -> ServerResult {
2054 task::block_in_place(|| {
2055 let (inscriptions, more) = index.get_inscriptions_paginated(100, page_index)?;
2056
2057 let prev = page_index.checked_sub(1);
2058
2059 let next = more.then_some(page_index + 1);
2060
2061 Ok(if accept_json {
2062 Json(api::Inscriptions {
2063 ids: inscriptions,
2064 page_index,
2065 more,
2066 })
2067 .into_response()
2068 } else {
2069 InscriptionsHtml {
2070 inscriptions,
2071 next,
2072 prev,
2073 }
2074 .page(server_config)
2075 .into_response()
2076 })
2077 })
2078 }
2079
2080 async fn inscriptions_in_block(
2081 Extension(server_config): Extension<Arc<ServerConfig>>,
2082 Extension(index): Extension<Arc<Index>>,
2083 Path(block_height): Path<u32>,
2084 AcceptJson(accept_json): AcceptJson,
2085 ) -> ServerResult {
2086 Self::inscriptions_in_block_paginated(
2087 Extension(server_config),
2088 Extension(index),
2089 Path((block_height, 0)),
2090 AcceptJson(accept_json),
2091 )
2092 .await
2093 }
2094
2095 async fn inscriptions_in_block_paginated(
2096 Extension(server_config): Extension<Arc<ServerConfig>>,
2097 Extension(index): Extension<Arc<Index>>,
2098 Path((block_height, page_index)): Path<(u32, u32)>,
2099 AcceptJson(accept_json): AcceptJson,
2100 ) -> ServerResult {
2101 task::block_in_place(|| {
2102 let mut inscriptions = index
2103 .get_inscriptions_in_block(block_height)?
2104 .into_iter()
2105 .skip(page_index.into_usize().saturating_mul(PAGE_SIZE))
2106 .take(PAGE_SIZE.saturating_add(1))
2107 .collect::<Vec<InscriptionId>>();
2108
2109 let more = inscriptions.len() > PAGE_SIZE;
2110
2111 if more {
2112 inscriptions.pop();
2113 }
2114
2115 Ok(if accept_json {
2116 Json(api::Inscriptions {
2117 ids: inscriptions,
2118 page_index,
2119 more,
2120 })
2121 .into_response()
2122 } else {
2123 InscriptionsBlockHtml::new(
2124 block_height,
2125 index.block_height()?.unwrap_or(Height(0)).n(),
2126 inscriptions,
2127 more,
2128 page_index,
2129 )
2130 .page(server_config)
2131 .into_response()
2132 })
2133 })
2134 }
2135
2136 async fn parents(
2137 Extension(server_config): Extension<Arc<ServerConfig>>,
2138 Extension(index): Extension<Arc<Index>>,
2139 Path(inscription_id): Path<InscriptionId>,
2140 ) -> ServerResult<Response> {
2141 Self::parents_paginated(
2142 Extension(server_config),
2143 Extension(index),
2144 Path((inscription_id, 0)),
2145 )
2146 .await
2147 }
2148
2149 async fn parents_paginated(
2150 Extension(server_config): Extension<Arc<ServerConfig>>,
2151 Extension(index): Extension<Arc<Index>>,
2152 Path((id, page)): Path<(InscriptionId, usize)>,
2153 ) -> ServerResult<Response> {
2154 task::block_in_place(|| {
2155 let child = index
2156 .get_inscription_entry(id)?
2157 .ok_or_not_found(|| format!("inscription {id}"))?;
2158
2159 let (parents, more) =
2160 index.get_parents_by_sequence_number_paginated(child.parents, PAGE_SIZE, page)?;
2161
2162 let prev_page = page.checked_sub(1);
2163
2164 let next_page = more.then_some(page + 1);
2165
2166 Ok(
2167 ParentsHtml {
2168 id,
2169 number: child.inscription_number,
2170 parents,
2171 prev_page,
2172 next_page,
2173 }
2174 .page(server_config)
2175 .into_response(),
2176 )
2177 })
2178 }
2179
2180 fn proxy(proxy: &Url, path: &str) -> ServerResult<Response> {
2181 let response = reqwest::blocking::Client::new()
2182 .get(format!("{}{}", proxy, &path[1..]))
2183 .send()
2184 .map_err(|err| anyhow!(err))?;
2185
2186 let status = response.status();
2187
2188 let mut headers = response.headers().clone();
2189
2190 headers.insert(
2191 header::CONTENT_SECURITY_POLICY,
2192 HeaderValue::from_str(&format!(
2193 "default-src 'self' {proxy} 'unsafe-eval' 'unsafe-inline' data: blob:"
2194 ))
2195 .map_err(|err| ServerError::Internal(Error::from(err)))?,
2196 );
2197
2198 Ok(
2199 (
2200 status,
2201 headers,
2202 response.bytes().map_err(|err| anyhow!(err))?,
2203 )
2204 .into_response(),
2205 )
2206 }
2207
2208 async fn redirect_http_to_https(
2209 Extension(mut destination): Extension<String>,
2210 uri: Uri,
2211 ) -> Redirect {
2212 if let Some(path_and_query) = uri.path_and_query() {
2213 destination.push_str(path_and_query.as_str());
2214 }
2215
2216 Redirect::to(&destination)
2217 }
2218}
2219
2220#[cfg(test)]
2221mod tests {
2222 use {
2223 super::*,
2224 reqwest::{
2225 StatusCode, Url,
2226 header::{self, HeaderMap},
2227 },
2228 serde::de::DeserializeOwned,
2229 tempfile::TempDir,
2230 };
2231
2232 const RUNE: u128 = 99246114928149462;
2233
2234 #[derive(Default)]
2235 struct Builder {
2236 core: Option<mockcore::Handle>,
2237 config: String,
2238 ord_args: BTreeMap<String, Option<String>>,
2239 server_args: BTreeMap<String, Option<String>>,
2240 }
2241
2242 impl Builder {
2243 fn core(self, core: mockcore::Handle) -> Self {
2244 Self {
2245 core: Some(core),
2246 ..self
2247 }
2248 }
2249
2250 fn ord_option(mut self, option: &str, value: &str) -> Self {
2251 self.ord_args.insert(option.into(), Some(value.into()));
2252 self
2253 }
2254
2255 fn ord_flag(mut self, flag: &str) -> Self {
2256 self.ord_args.insert(flag.into(), None);
2257 self
2258 }
2259
2260 fn server_option(mut self, option: &str, value: &str) -> Self {
2261 self.server_args.insert(option.into(), Some(value.into()));
2262 self
2263 }
2264
2265 fn server_flag(mut self, flag: &str) -> Self {
2266 self.server_args.insert(flag.into(), None);
2267 self
2268 }
2269
2270 fn chain(self, chain: Chain) -> Self {
2271 self.ord_option("--chain", &chain.to_string())
2272 }
2273
2274 fn config(self, config: &str) -> Self {
2275 Self {
2276 config: config.into(),
2277 ..self
2278 }
2279 }
2280
2281 fn build(self) -> TestServer {
2282 let core = self.core.unwrap_or_else(|| {
2283 mockcore::builder()
2284 .network(
2285 self
2286 .ord_args
2287 .get("--chain")
2288 .map(|chain| chain.as_ref().unwrap().parse::<Chain>().unwrap())
2289 .unwrap_or_default()
2290 .network(),
2291 )
2292 .build()
2293 });
2294
2295 let tempdir = TempDir::new().unwrap();
2296
2297 let cookiefile = tempdir.path().join("cookie");
2298
2299 fs::write(&cookiefile, "username:password").unwrap();
2300
2301 let mut args = vec!["ord".to_string()];
2302
2303 args.push("--bitcoin-rpc-url".into());
2304 args.push(core.url());
2305
2306 args.push("--cookie-file".into());
2307 args.push(cookiefile.to_str().unwrap().into());
2308
2309 args.push("--datadir".into());
2310 args.push(tempdir.path().to_str().unwrap().into());
2311
2312 if !self.ord_args.contains_key("--chain") {
2313 args.push("--chain".into());
2314 args.push(core.network());
2315 }
2316
2317 for (arg, value) in self.ord_args {
2318 args.push(arg);
2319
2320 if let Some(value) = value {
2321 args.push(value);
2322 }
2323 }
2324
2325 args.push("server".into());
2326
2327 args.push("--address".into());
2328 args.push("127.0.0.1".into());
2329
2330 args.push("--http-port".into());
2331 args.push("0".to_string());
2332
2333 args.push("--polling-interval".into());
2334 args.push("100ms".into());
2335
2336 for (arg, value) in self.server_args {
2337 args.push(arg);
2338
2339 if let Some(value) = value {
2340 args.push(value);
2341 }
2342 }
2343
2344 let arguments = Arguments::try_parse_from(args).unwrap();
2345
2346 let Subcommand::Server(server) = arguments.subcommand else {
2347 panic!("unexpected subcommand: {:?}", arguments.subcommand);
2348 };
2349
2350 let settings = Settings::from_options(arguments.options)
2351 .or(serde_yaml::from_str::<Settings>(&self.config).unwrap())
2352 .or_defaults()
2353 .unwrap();
2354
2355 let index = Arc::new(Index::open(&settings).unwrap());
2356 let ord_server_handle = Handle::new();
2357
2358 let (tx, rx) = std::sync::mpsc::channel();
2359
2360 {
2361 let index = index.clone();
2362 let ord_server_handle = ord_server_handle.clone();
2363 thread::spawn(|| {
2364 server
2365 .run(settings, index, ord_server_handle, Some(tx))
2366 .unwrap()
2367 });
2368 }
2369
2370 while index.statistic(crate::index::Statistic::Commits) == 0 {
2371 thread::sleep(Duration::from_millis(50));
2372 }
2373
2374 let port = rx.recv().unwrap();
2375
2376 TestServer {
2377 core,
2378 index,
2379 ord_server_handle,
2380 tempdir,
2381 url: Url::parse(&format!("http://127.0.0.1:{port}")).unwrap(),
2382 }
2383 }
2384
2385 fn https(self) -> Self {
2386 self.server_flag("--https")
2387 }
2388
2389 fn index_addresses(self) -> Self {
2390 self.ord_flag("--index-addresses")
2391 }
2392
2393 fn index_runes(self) -> Self {
2394 self.ord_flag("--index-runes")
2395 }
2396
2397 fn index_sats(self) -> Self {
2398 self.ord_flag("--index-sats")
2399 }
2400
2401 fn redirect_http_to_https(self) -> Self {
2402 self.server_flag("--redirect-http-to-https")
2403 }
2404 }
2405
2406 struct TestServer {
2407 core: mockcore::Handle,
2408 index: Arc<Index>,
2409 ord_server_handle: Handle<SocketAddr>,
2410 #[allow(unused)]
2411 tempdir: TempDir,
2412 url: Url,
2413 }
2414
2415 impl TestServer {
2416 fn builder() -> Builder {
2417 Default::default()
2418 }
2419
2420 fn new() -> Self {
2421 Builder::default().build()
2422 }
2423
2424 #[track_caller]
2425 pub(crate) fn etch(
2426 &self,
2427 runestone: Runestone,
2428 outputs: usize,
2429 witness: Option<Witness>,
2430 ) -> (Txid, RuneId) {
2431 let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap();
2432
2433 self.mine_blocks(1);
2434
2435 self.core.broadcast_tx(TransactionTemplate {
2436 inputs: &[(block_count, 0, 0, Default::default())],
2437 p2tr: true,
2438 ..default()
2439 });
2440
2441 self.mine_blocks((Runestone::COMMIT_CONFIRMATIONS - 1).into());
2442
2443 let witness = witness.unwrap_or_else(|| {
2444 let tapscript = script::Builder::new()
2445 .push_slice::<&PushBytes>(
2446 runestone
2447 .etching
2448 .unwrap()
2449 .rune
2450 .unwrap()
2451 .commitment()
2452 .as_slice()
2453 .try_into()
2454 .unwrap(),
2455 )
2456 .into_script();
2457 let mut witness = Witness::default();
2458 witness.push(tapscript);
2459 witness.push([]);
2460 witness
2461 });
2462
2463 let txid = self.core.broadcast_tx(TransactionTemplate {
2464 inputs: &[(block_count + 1, 1, 0, witness)],
2465 op_return: Some(runestone.encipher()),
2466 outputs,
2467 ..default()
2468 });
2469
2470 self.mine_blocks(1);
2471
2472 (
2473 txid,
2474 RuneId {
2475 block: (self.index.block_count().unwrap() - 1).into(),
2476 tx: 1,
2477 },
2478 )
2479 }
2480
2481 #[track_caller]
2482 fn get(&self, path: impl AsRef<str>) -> reqwest::blocking::Response {
2483 if let Err(error) = self.index.update() {
2484 log::error!("{error}");
2485 }
2486 reqwest::blocking::get(self.join_url(path.as_ref())).unwrap()
2487 }
2488
2489 #[track_caller]
2490 pub(crate) fn get_json<T: DeserializeOwned>(&self, path: impl AsRef<str>) -> T {
2491 if let Err(error) = self.index.update() {
2492 log::error!("{error}");
2493 }
2494
2495 let client = reqwest::blocking::Client::new();
2496
2497 let response = client
2498 .get(self.join_url(path.as_ref()))
2499 .header(header::ACCEPT, "application/json")
2500 .send()
2501 .unwrap();
2502
2503 assert_eq!(response.status(), StatusCode::OK);
2504
2505 response.json().unwrap()
2506 }
2507
2508 #[track_caller]
2509 fn post_json<T: DeserializeOwned>(&self, path: impl AsRef<str>, body: &impl Serialize) -> T {
2510 if let Err(error) = self.index.update() {
2511 log::error!("{error}");
2512 }
2513
2514 let client = reqwest::blocking::Client::new();
2515
2516 let response = client
2517 .post(self.join_url(path.as_ref()))
2518 .json(body)
2519 .header(header::ACCEPT, "application/json")
2520 .send()
2521 .unwrap();
2522
2523 assert_eq!(response.status(), StatusCode::OK);
2524
2525 response.json().unwrap()
2526 }
2527
2528 #[track_caller]
2529 fn post(
2530 &self,
2531 path: impl AsRef<str>,
2532 body: &str,
2533 status: StatusCode,
2534 ) -> reqwest::blocking::Response {
2535 if let Err(error) = self.index.update() {
2536 log::error!("{error}");
2537 }
2538
2539 let client = reqwest::blocking::Client::new();
2540
2541 let response = client
2542 .post(self.join_url(path.as_ref()))
2543 .body(body.as_bytes().to_vec())
2544 .send()
2545 .unwrap();
2546
2547 assert_eq!(response.status(), status, "{}", response.text().unwrap());
2548
2549 response
2550 }
2551
2552 fn join_url(&self, url: &str) -> Url {
2553 self.url.join(url).unwrap()
2554 }
2555
2556 #[track_caller]
2557 fn assert_response(&self, path: impl AsRef<str>, status: StatusCode, expected_response: &str) {
2558 let response = self.get(path);
2559 assert_eq!(response.status(), status, "{}", response.text().unwrap());
2560 pretty_assert_eq!(response.text().unwrap(), expected_response);
2561 }
2562
2563 #[track_caller]
2564 fn assert_response_regex(
2565 &self,
2566 path: impl AsRef<str>,
2567 status: StatusCode,
2568 regex: impl AsRef<str>,
2569 ) {
2570 let response = self.get(path);
2571 assert_eq!(
2572 response.status(),
2573 status,
2574 "response: {}",
2575 response.text().unwrap()
2576 );
2577 assert_regex_match!(response.text().unwrap(), regex.as_ref());
2578 }
2579
2580 #[track_caller]
2581 fn assert_html(&self, path: impl AsRef<str>, content: impl PageContent) {
2582 self.assert_html_status(path, StatusCode::OK, content);
2583 }
2584
2585 #[track_caller]
2586 fn assert_html_status(
2587 &self,
2588 path: impl AsRef<str>,
2589 status: StatusCode,
2590 content: impl PageContent,
2591 ) {
2592 let response = self.get(path);
2593
2594 assert_eq!(response.status(), status, "{}", response.text().unwrap());
2595
2596 let expected_response = PageHtml::new(
2597 content,
2598 Arc::new(ServerConfig {
2599 chain: self.index.chain(),
2600 domain: Some(System::host_name().unwrap()),
2601 ..Default::default()
2602 }),
2603 )
2604 .to_string();
2605
2606 pretty_assert_eq!(response.text().unwrap(), expected_response);
2607 }
2608
2609 fn assert_response_csp(
2610 &self,
2611 path: impl AsRef<str>,
2612 status: StatusCode,
2613 content_security_policy: &str,
2614 regex: impl AsRef<str>,
2615 ) {
2616 let response = self.get(path);
2617 assert_eq!(response.status(), status);
2618 assert_eq!(
2619 response
2620 .headers()
2621 .get(header::CONTENT_SECURITY_POLICY,)
2622 .unwrap(),
2623 content_security_policy
2624 );
2625 assert_regex_match!(response.text().unwrap(), regex.as_ref());
2626 }
2627
2628 #[track_caller]
2629 fn assert_redirect(&self, path: &str, location: &str) {
2630 let response = reqwest::blocking::Client::builder()
2631 .redirect(reqwest::redirect::Policy::none())
2632 .build()
2633 .unwrap()
2634 .get(self.join_url(path))
2635 .send()
2636 .unwrap();
2637
2638 assert_eq!(response.status(), StatusCode::SEE_OTHER);
2639 assert_eq!(response.headers().get(header::LOCATION).unwrap(), location);
2640 }
2641
2642 #[track_caller]
2643 fn mine_blocks(&self, n: u64) -> Vec<Block> {
2644 let blocks = self.core.mine_blocks(n);
2645 self.index.update().unwrap();
2646 blocks
2647 }
2648
2649 fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec<Block> {
2650 let blocks = self.core.mine_blocks_with_subsidy(n, subsidy);
2651 self.index.update().unwrap();
2652 blocks
2653 }
2654 }
2655
2656 impl Drop for TestServer {
2657 fn drop(&mut self) {
2658 self.ord_server_handle.shutdown();
2659 }
2660 }
2661
2662 fn parse_server_args(args: &str) -> (Settings, Server) {
2663 match Arguments::try_parse_from(args.split_whitespace()) {
2664 Ok(arguments) => match arguments.subcommand {
2665 Subcommand::Server(server) => (
2666 Settings::from_options(arguments.options)
2667 .or_defaults()
2668 .unwrap(),
2669 server,
2670 ),
2671 subcommand => panic!("unexpected subcommand: {subcommand:?}"),
2672 },
2673 Err(err) => panic!("error parsing arguments: {err}"),
2674 }
2675 }
2676
2677 #[test]
2678 fn http_and_https_port_dont_conflict() {
2679 parse_server_args(
2680 "ord server --http-port 0 --https-port 0 --acme-cache foo --acme-contact bar --acme-domain baz",
2681 );
2682 }
2683
2684 #[test]
2685 fn http_port_defaults_to_80() {
2686 assert_eq!(parse_server_args("ord server").1.http_port(), Some(80));
2687 }
2688
2689 #[test]
2690 fn https_port_defaults_to_none() {
2691 assert_eq!(parse_server_args("ord server").1.https_port(), None);
2692 }
2693
2694 #[test]
2695 fn https_sets_https_port_to_443() {
2696 assert_eq!(
2697 parse_server_args("ord server --https --acme-cache foo --acme-contact bar --acme-domain baz")
2698 .1
2699 .https_port(),
2700 Some(443)
2701 );
2702 }
2703
2704 #[test]
2705 fn https_disables_http() {
2706 assert_eq!(
2707 parse_server_args("ord server --https --acme-cache foo --acme-contact bar --acme-domain baz")
2708 .1
2709 .http_port(),
2710 None
2711 );
2712 }
2713
2714 #[test]
2715 fn https_port_disables_http() {
2716 assert_eq!(
2717 parse_server_args(
2718 "ord server --https-port 433 --acme-cache foo --acme-contact bar --acme-domain baz"
2719 )
2720 .1
2721 .http_port(),
2722 None
2723 );
2724 }
2725
2726 #[test]
2727 fn https_port_sets_https_port() {
2728 assert_eq!(
2729 parse_server_args(
2730 "ord server --https-port 1000 --acme-cache foo --acme-contact bar --acme-domain baz"
2731 )
2732 .1
2733 .https_port(),
2734 Some(1000)
2735 );
2736 }
2737
2738 #[test]
2739 fn http_with_https_leaves_http_enabled() {
2740 assert_eq!(
2741 parse_server_args(
2742 "ord server --https --http --acme-cache foo --acme-contact bar --acme-domain baz"
2743 )
2744 .1
2745 .http_port(),
2746 Some(80)
2747 );
2748 }
2749
2750 #[test]
2751 fn http_with_https_leaves_https_enabled() {
2752 assert_eq!(
2753 parse_server_args(
2754 "ord server --https --http --acme-cache foo --acme-contact bar --acme-domain baz"
2755 )
2756 .1
2757 .https_port(),
2758 Some(443)
2759 );
2760 }
2761
2762 #[test]
2763 fn acme_contact_accepts_multiple_values() {
2764 assert!(
2765 Arguments::try_parse_from([
2766 "ord",
2767 "server",
2768 "--address",
2769 "127.0.0.1",
2770 "--http-port",
2771 "0",
2772 "--acme-contact",
2773 "foo",
2774 "--acme-contact",
2775 "bar"
2776 ])
2777 .is_ok()
2778 );
2779 }
2780
2781 #[test]
2782 fn acme_domain_accepts_multiple_values() {
2783 assert!(
2784 Arguments::try_parse_from([
2785 "ord",
2786 "server",
2787 "--address",
2788 "127.0.0.1",
2789 "--http-port",
2790 "0",
2791 "--acme-domain",
2792 "foo",
2793 "--acme-domain",
2794 "bar"
2795 ])
2796 .is_ok()
2797 );
2798 }
2799
2800 #[test]
2801 fn acme_cache_defaults_to_data_dir() {
2802 let arguments = Arguments::try_parse_from(["ord", "--datadir", "foo", "server"]).unwrap();
2803
2804 let settings = Settings::from_options(arguments.options)
2805 .or_defaults()
2806 .unwrap();
2807
2808 let acme_cache = Server::acme_cache(None, &settings).display().to_string();
2809 assert!(
2810 acme_cache.contains(if cfg!(windows) {
2811 r"foo\acme-cache"
2812 } else {
2813 "foo/acme-cache"
2814 }),
2815 "{acme_cache}"
2816 )
2817 }
2818
2819 #[test]
2820 fn acme_cache_flag_is_respected() {
2821 let arguments =
2822 Arguments::try_parse_from(["ord", "--datadir", "foo", "server", "--acme-cache", "bar"])
2823 .unwrap();
2824
2825 let settings = Settings::from_options(arguments.options)
2826 .or_defaults()
2827 .unwrap();
2828
2829 let acme_cache = Server::acme_cache(Some(&"bar".into()), &settings)
2830 .display()
2831 .to_string();
2832 assert_eq!(acme_cache, "bar")
2833 }
2834
2835 #[test]
2836 fn acme_domain_defaults_to_hostname() {
2837 let (_, server) = parse_server_args("ord server");
2838 assert_eq!(
2839 server.acme_domains().unwrap(),
2840 &[System::host_name().unwrap()]
2841 );
2842 }
2843
2844 #[test]
2845 fn acme_domain_flag_is_respected() {
2846 let (_, server) = parse_server_args("ord server --acme-domain example.com");
2847 assert_eq!(server.acme_domains().unwrap(), &["example.com"]);
2848 }
2849
2850 #[test]
2851 fn install_sh_redirects_to_github() {
2852 TestServer::new().assert_redirect(
2853 "/install.sh",
2854 "https://raw.githubusercontent.com/ordinals/ord/master/install.sh",
2855 );
2856 }
2857
2858 #[test]
2859 fn ordinal_redirects_to_sat() {
2860 TestServer::new().assert_redirect("/ordinal/0", "/sat/0");
2861 }
2862
2863 #[test]
2864 fn bounties_redirects_to_docs_site() {
2865 TestServer::new().assert_redirect("/bounties", "https://docs.ordinals.com/bounties");
2866 }
2867
2868 #[test]
2869 fn faq_redirects_to_docs_site() {
2870 TestServer::new().assert_redirect("/faq", "https://docs.ordinals.com/faq");
2871 }
2872
2873 #[test]
2874 fn search_by_query_returns_rune() {
2875 TestServer::new().assert_redirect("/search?query=ABCD", "/rune/ABCD");
2876 }
2877
2878 #[test]
2879 fn search_by_query_returns_spaced_rune() {
2880 TestServer::new().assert_redirect("/search?query=AB•CD", "/rune/AB•CD");
2881 }
2882
2883 #[test]
2884 fn search_by_query_returns_satscard() {
2885 TestServer::new().assert_redirect(
2886 "/search?query=https://satscard.com/start%23foo",
2887 "/satscard?foo",
2888 );
2889 TestServer::new().assert_redirect(
2890 "/search?query=https://getsatscard.com/start%23foo",
2891 "/satscard?foo",
2892 );
2893 TestServer::new().assert_redirect(
2894 "/search?query=https://ordinals.com/satscard?foo",
2895 "/satscard?foo",
2896 );
2897 }
2898
2899 #[test]
2900 fn search_by_query_returns_inscription() {
2901 TestServer::new().assert_redirect(
2902 "/search?query=0000000000000000000000000000000000000000000000000000000000000000i0",
2903 "/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
2904 );
2905 }
2906
2907 #[test]
2908 fn search_by_query_returns_inscription_by_number() {
2909 TestServer::new().assert_redirect("/search?query=0", "/inscription/0");
2910 }
2911
2912 #[test]
2913 fn search_is_whitespace_insensitive() {
2914 TestServer::new().assert_redirect("/search/ abc ", "/sat/abc");
2915 }
2916
2917 #[test]
2918 fn search_by_path_returns_sat() {
2919 TestServer::new().assert_redirect("/search/abc", "/sat/abc");
2920 }
2921
2922 #[test]
2923 fn search_for_blockhash_returns_block() {
2924 TestServer::new().assert_redirect(
2925 "/search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
2926 "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
2927 );
2928 }
2929
2930 #[test]
2931 fn search_for_txid_returns_transaction() {
2932 TestServer::new().assert_redirect(
2933 "/search/0000000000000000000000000000000000000000000000000000000000000000",
2934 "/tx/0000000000000000000000000000000000000000000000000000000000000000",
2935 );
2936 }
2937
2938 #[test]
2939 fn search_for_outpoint_returns_output() {
2940 TestServer::new().assert_redirect(
2941 "/search/0000000000000000000000000000000000000000000000000000000000000000:0",
2942 "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
2943 );
2944 }
2945
2946 #[test]
2947 fn search_for_inscription_id_returns_inscription() {
2948 TestServer::new().assert_redirect(
2949 "/search/0000000000000000000000000000000000000000000000000000000000000000i0",
2950 "/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
2951 );
2952 }
2953
2954 #[test]
2955 fn search_by_path_returns_rune() {
2956 TestServer::new().assert_redirect("/search/ABCD", "/rune/ABCD");
2957 }
2958
2959 #[test]
2960 fn search_by_path_returns_spaced_rune() {
2961 TestServer::new().assert_redirect("/search/AB•CD", "/rune/AB•CD");
2962 }
2963
2964 #[test]
2965 fn search_by_rune_id_returns_rune() {
2966 let server = TestServer::builder()
2967 .chain(Chain::Regtest)
2968 .index_runes()
2969 .build();
2970
2971 server.mine_blocks(1);
2972
2973 let rune = Rune(RUNE);
2974
2975 server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
2976
2977 server.etch(
2978 Runestone {
2979 edicts: vec![Edict {
2980 id: RuneId::default(),
2981 amount: u128::MAX,
2982 output: 0,
2983 }],
2984 etching: Some(Etching {
2985 rune: Some(rune),
2986 ..default()
2987 }),
2988 ..default()
2989 },
2990 1,
2991 None,
2992 );
2993
2994 server.mine_blocks(1);
2995
2996 server.assert_redirect("/search/8:1", "/rune/AAAAAAAAAAAAA");
2997 server.assert_redirect("/search?query=8:1", "/rune/AAAAAAAAAAAAA");
2998
2999 server.assert_response_regex(
3000 "/search/100000000000000000000:200000000000000000",
3001 StatusCode::BAD_REQUEST,
3002 ".*",
3003 );
3004 }
3005
3006 #[test]
3007 fn search_by_satpoint_returns_sat() {
3008 let server = TestServer::builder()
3009 .chain(Chain::Regtest)
3010 .index_sats()
3011 .build();
3012
3013 let txid = server.mine_blocks(1)[0].txdata[0].compute_txid();
3014
3015 server.assert_redirect(
3016 &format!("/search/{txid}:0:0"),
3017 &format!("/satpoint/{txid}:0:0"),
3018 );
3019
3020 server.assert_redirect(
3021 &format!("/search?query={txid}:0:0"),
3022 &format!("/satpoint/{txid}:0:0"),
3023 );
3024
3025 server.assert_redirect(
3026 &format!("/satpoint/{txid}:0:0"),
3027 &format!("/sat/{}", 50 * COIN_VALUE),
3028 );
3029
3030 server.assert_response_regex("/search/1:2:3", StatusCode::BAD_REQUEST, ".*");
3031 }
3032
3033 #[test]
3034 fn satpoint_returns_sat_in_multiple_ranges() {
3035 let server = TestServer::builder()
3036 .chain(Chain::Regtest)
3037 .index_sats()
3038 .build();
3039
3040 server.mine_blocks(1);
3041
3042 let split = TransactionTemplate {
3043 inputs: &[(1, 0, 0, Default::default())],
3044 outputs: 2,
3045 fee: 0,
3046 ..default()
3047 };
3048
3049 server.core.broadcast_tx(split);
3050
3051 server.mine_blocks(1);
3052
3053 let merge = TransactionTemplate {
3054 inputs: &[(2, 0, 0, Default::default()), (2, 1, 0, Default::default())],
3055 fee: 0,
3056 ..default()
3057 };
3058
3059 let txid = server.core.broadcast_tx(merge);
3060
3061 server.mine_blocks(1);
3062
3063 server.assert_redirect(
3064 &format!("/satpoint/{txid}:0:0"),
3065 &format!("/sat/{}", 100 * COIN_VALUE),
3066 );
3067
3068 server.assert_redirect(
3069 &format!("/satpoint/{txid}:0:{}", 50 * COIN_VALUE),
3070 &format!("/sat/{}", 50 * COIN_VALUE),
3071 );
3072
3073 server.assert_redirect(
3074 &format!("/satpoint/{txid}:0:{}", 50 * COIN_VALUE - 1),
3075 &format!("/sat/{}", 150 * COIN_VALUE - 1),
3076 );
3077 }
3078
3079 #[test]
3080 fn fallback() {
3081 let server = TestServer::new();
3082
3083 server.assert_redirect("/0", "/inscription/0");
3084 server.assert_redirect("/0/", "/inscription/0");
3085 server.assert_redirect("/0//", "/inscription/0");
3086 server.assert_redirect(
3087 "/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0",
3088 "/inscription/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0",
3089 );
3090 server.assert_redirect("/-1", "/inscription/-1");
3091 server.assert_redirect("/FOO", "/rune/FOO");
3092 server.assert_redirect("/FO.O", "/rune/FO.O");
3093 server.assert_redirect("/FO•O", "/rune/FO•O");
3094 server.assert_redirect("/0:0", "/rune/0:0");
3095 server.assert_redirect(
3096 "/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
3097 "/output/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
3098 );
3099 server.assert_redirect(
3100 "/4273262611454246626680278280877079635139930168289368354696278617:0",
3101 "/output/4273262611454246626680278280877079635139930168289368354696278617:0",
3102 );
3103 server.assert_redirect(
3104 "/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0",
3105 "/satpoint/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0",
3106 );
3107 server.assert_redirect(
3108 "/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
3109 "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
3110 );
3111 server.assert_redirect(
3112 "/000000000000000000000000000000000000000000000000000000000000000f",
3113 "/tx/000000000000000000000000000000000000000000000000000000000000000f",
3114 );
3115 server.assert_redirect(
3116 "/4273262611454246626680278280877079635139930168289368354696278617",
3117 "/tx/4273262611454246626680278280877079635139930168289368354696278617",
3118 );
3119 server.assert_redirect(
3120 "/bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297",
3121 "/address/bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297",
3122 );
3123 server.assert_redirect(
3124 "/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
3125 "/address/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
3126 );
3127 server.assert_redirect(
3128 "/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
3129 "/address/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
3130 );
3131
3132 server.assert_response_regex("/hello", StatusCode::NOT_FOUND, "");
3133
3134 server.assert_response_regex(
3135 "/%C3%28",
3136 StatusCode::BAD_REQUEST,
3137 "invalid utf-8 sequence of 1 bytes from index 0",
3138 );
3139 }
3140
3141 #[test]
3142 fn runes_can_be_queried_by_rune_id() {
3143 let server = TestServer::builder()
3144 .chain(Chain::Regtest)
3145 .index_runes()
3146 .build();
3147
3148 server.mine_blocks(1);
3149
3150 let rune = Rune(RUNE);
3151
3152 server.assert_response_regex("/rune/9:1", StatusCode::NOT_FOUND, ".*");
3153
3154 server.etch(
3155 Runestone {
3156 edicts: vec![Edict {
3157 id: RuneId::default(),
3158 amount: u128::MAX,
3159 output: 0,
3160 }],
3161 etching: Some(Etching {
3162 rune: Some(rune),
3163 ..default()
3164 }),
3165 ..default()
3166 },
3167 1,
3168 None,
3169 );
3170
3171 server.mine_blocks(1);
3172
3173 server.assert_response_regex(
3174 "/rune/8:1",
3175 StatusCode::OK,
3176 ".*<title>Rune AAAAAAAAAAAAA</title>.*",
3177 );
3178 }
3179
3180 #[test]
3181 fn runes_can_be_queried_by_rune_number() {
3182 let server = TestServer::builder()
3183 .chain(Chain::Regtest)
3184 .index_runes()
3185 .build();
3186
3187 server.mine_blocks(1);
3188
3189 server.assert_response_regex("/rune/0", StatusCode::NOT_FOUND, ".*");
3190
3191 for i in 0..10 {
3192 let rune = Rune(RUNE + i);
3193 server.etch(
3194 Runestone {
3195 edicts: vec![Edict {
3196 id: RuneId::default(),
3197 amount: u128::MAX,
3198 output: 0,
3199 }],
3200 etching: Some(Etching {
3201 rune: Some(rune),
3202 ..default()
3203 }),
3204 ..default()
3205 },
3206 1,
3207 None,
3208 );
3209
3210 server.mine_blocks(1);
3211 }
3212
3213 server.assert_response_regex(
3214 "/rune/0",
3215 StatusCode::OK,
3216 ".*<title>Rune AAAAAAAAAAAAA</title>.*",
3217 );
3218
3219 for i in 1..6 {
3220 server.assert_response_regex(
3221 format!("/rune/{i}"),
3222 StatusCode::OK,
3223 ".*<title>Rune AAAAAAAAAAAA.*</title>.*",
3224 );
3225 }
3226
3227 server.assert_response_regex(
3228 "/rune/9",
3229 StatusCode::OK,
3230 ".*<title>Rune AAAAAAAAAAAAJ</title>.*",
3231 );
3232 }
3233
3234 #[test]
3235 fn rune_not_etched_shows_unlock_height() {
3236 let server = TestServer::builder()
3237 .chain(Chain::Regtest)
3238 .index_runes()
3239 .build();
3240
3241 server.mine_blocks(1);
3242
3243 server.assert_html_status(
3244 "/rune/A",
3245 StatusCode::NOT_FOUND,
3246 RuneNotFoundHtml {
3247 rune: Rune(0),
3248 unlock: Some((
3249 Height(209999),
3250 Blocktime::Expected(DateTime::from_timestamp(125998800, 0).unwrap()),
3251 )),
3252 },
3253 );
3254 }
3255
3256 #[test]
3257 fn reserved_rune_not_etched_shows_reserved_status() {
3258 let server = TestServer::builder()
3259 .chain(Chain::Regtest)
3260 .index_runes()
3261 .build();
3262
3263 server.mine_blocks(1);
3264
3265 server.assert_html_status(
3266 format!("/rune/{}", Rune(Rune::RESERVED)),
3267 StatusCode::NOT_FOUND,
3268 RuneNotFoundHtml {
3269 rune: Rune(Rune::RESERVED),
3270 unlock: None,
3271 },
3272 );
3273 }
3274
3275 #[test]
3276 fn runes_are_displayed_on_runes_page() {
3277 let server = TestServer::builder()
3278 .chain(Chain::Regtest)
3279 .index_runes()
3280 .build();
3281
3282 server.mine_blocks(1);
3283
3284 server.assert_html(
3285 "/runes",
3286 RunesHtml {
3287 entries: Vec::new(),
3288 more: false,
3289 prev: None,
3290 next: None,
3291 },
3292 );
3293
3294 let (txid, id) = server.etch(
3295 Runestone {
3296 edicts: vec![Edict {
3297 id: RuneId::default(),
3298 amount: u128::MAX,
3299 output: 0,
3300 }],
3301 etching: Some(Etching {
3302 rune: Some(Rune(RUNE)),
3303 symbol: Some('%'),
3304 premine: Some(u128::MAX),
3305 ..default()
3306 }),
3307 ..default()
3308 },
3309 1,
3310 Default::default(),
3311 );
3312
3313 pretty_assert_eq!(
3314 server.index.runes().unwrap(),
3315 [(
3316 id,
3317 RuneEntry {
3318 block: id.block,
3319 etching: txid,
3320 spaced_rune: SpacedRune {
3321 rune: Rune(RUNE),
3322 spacers: 0
3323 },
3324 premine: u128::MAX,
3325 timestamp: id.block,
3326 symbol: Some('%'),
3327 ..default()
3328 }
3329 )]
3330 );
3331
3332 assert_eq!(
3333 server.index.get_rune_balances().unwrap(),
3334 [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
3335 );
3336
3337 server.assert_html(
3338 "/runes",
3339 RunesHtml {
3340 entries: vec![(
3341 RuneId::default(),
3342 RuneEntry {
3343 spaced_rune: SpacedRune {
3344 rune: Rune(RUNE),
3345 spacers: 0,
3346 },
3347 ..default()
3348 },
3349 )],
3350 more: false,
3351 prev: None,
3352 next: None,
3353 },
3354 );
3355 }
3356
3357 #[test]
3358 fn runes_are_displayed_on_rune_page() {
3359 let server = TestServer::builder()
3360 .chain(Chain::Regtest)
3361 .index_runes()
3362 .build();
3363
3364 server.mine_blocks(1);
3365
3366 let rune = Rune(RUNE);
3367
3368 server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
3369
3370 let (txid, id) = server.etch(
3371 Runestone {
3372 edicts: vec![Edict {
3373 id: RuneId::default(),
3374 amount: u128::MAX,
3375 output: 0,
3376 }],
3377 etching: Some(Etching {
3378 rune: Some(rune),
3379 symbol: Some('%'),
3380 premine: Some(u128::MAX),
3381 turbo: true,
3382 ..default()
3383 }),
3384 ..default()
3385 },
3386 1,
3387 Some(
3388 Inscription {
3389 content_type: Some("text/plain".into()),
3390 body: Some("hello".into()),
3391 rune: Some(rune.commitment()),
3392 ..default()
3393 }
3394 .to_witness(),
3395 ),
3396 );
3397
3398 let entry = RuneEntry {
3399 block: id.block,
3400 etching: txid,
3401 spaced_rune: SpacedRune { rune, spacers: 0 },
3402 premine: u128::MAX,
3403 symbol: Some('%'),
3404 timestamp: id.block,
3405 turbo: true,
3406 ..default()
3407 };
3408
3409 assert_eq!(server.index.runes().unwrap(), [(id, entry)]);
3410
3411 assert_eq!(
3412 server.index.get_rune_balances().unwrap(),
3413 [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
3414 );
3415
3416 let parent = InscriptionId { txid, index: 0 };
3417
3418 server.assert_html(
3419 format!("/rune/{rune}"),
3420 RuneHtml {
3421 id,
3422 entry,
3423 mintable: false,
3424 parent: Some(parent),
3425 },
3426 );
3427
3428 server.assert_response_regex(
3429 format!("/inscription/{parent}"),
3430 StatusCode::OK,
3431 ".*
3432<dl>
3433 <dt>rune</dt>
3434 <dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>
3435 .*
3436</dl>
3437.*",
3438 );
3439 }
3440
3441 #[test]
3442 fn etched_runes_are_displayed_on_block_page() {
3443 let server = TestServer::builder()
3444 .chain(Chain::Regtest)
3445 .index_runes()
3446 .build();
3447
3448 server.mine_blocks(1);
3449
3450 let rune0 = Rune(RUNE);
3451
3452 let (_txid, id) = server.etch(
3453 Runestone {
3454 edicts: vec![Edict {
3455 id: RuneId::default(),
3456 amount: u128::MAX,
3457 output: 0,
3458 }],
3459 etching: Some(Etching {
3460 rune: Some(rune0),
3461 ..default()
3462 }),
3463 ..default()
3464 },
3465 1,
3466 None,
3467 );
3468
3469 assert_eq!(
3470 server.index.get_runes_in_block(id.block - 1).unwrap().len(),
3471 0
3472 );
3473 assert_eq!(server.index.get_runes_in_block(id.block).unwrap().len(), 1);
3474 assert_eq!(
3475 server.index.get_runes_in_block(id.block + 1).unwrap().len(),
3476 0
3477 );
3478
3479 server.assert_response_regex(
3480 format!("/block/{}", id.block),
3481 StatusCode::OK,
3482 format!(".*<h2>1 Rune</h2>.*<li><a href=/rune/{rune0}>{rune0}</a></li>.*"),
3483 );
3484 }
3485
3486 #[test]
3487 fn runes_are_spaced() {
3488 let server = TestServer::builder()
3489 .chain(Chain::Regtest)
3490 .index_runes()
3491 .build();
3492
3493 server.mine_blocks(1);
3494
3495 let rune = Rune(RUNE);
3496
3497 server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
3498
3499 let (txid, id) = server.etch(
3500 Runestone {
3501 edicts: vec![Edict {
3502 id: RuneId::default(),
3503 amount: u128::MAX,
3504 output: 0,
3505 }],
3506 etching: Some(Etching {
3507 rune: Some(rune),
3508 symbol: Some('%'),
3509 spacers: Some(1),
3510 premine: Some(u128::MAX),
3511 ..default()
3512 }),
3513 ..default()
3514 },
3515 1,
3516 Some(
3517 Inscription {
3518 content_type: Some("text/plain".into()),
3519 body: Some("hello".into()),
3520 rune: Some(rune.commitment()),
3521 ..default()
3522 }
3523 .to_witness(),
3524 ),
3525 );
3526
3527 pretty_assert_eq!(
3528 server.index.runes().unwrap(),
3529 [(
3530 id,
3531 RuneEntry {
3532 block: id.block,
3533 etching: txid,
3534 spaced_rune: SpacedRune { rune, spacers: 1 },
3535 premine: u128::MAX,
3536 symbol: Some('%'),
3537 timestamp: id.block,
3538 ..default()
3539 }
3540 )]
3541 );
3542
3543 assert_eq!(
3544 server.index.get_rune_balances().unwrap(),
3545 [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
3546 );
3547
3548 server.assert_response_regex(
3549 format!("/rune/{rune}"),
3550 StatusCode::OK,
3551 r".*<title>Rune A•AAAAAAAAAAAA</title>.*<h1>A•AAAAAAAAAAAA</h1>.*",
3552 );
3553
3554 server.assert_response_regex(
3555 format!("/inscription/{txid}i0"),
3556 StatusCode::OK,
3557 ".*<dt>rune</dt>.*<dd><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></dd>.*",
3558 );
3559
3560 server.assert_response_regex(
3561 "/runes",
3562 StatusCode::OK,
3563 ".*<li><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></li>.*",
3564 );
3565
3566 server.assert_response_regex(
3567 format!("/tx/{txid}"),
3568 StatusCode::OK,
3569 ".*
3570 <dt>etching</dt>
3571 <dd><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></dd>
3572.*",
3573 );
3574
3575 server.assert_response_regex(
3576 format!("/output/{txid}:0"),
3577 StatusCode::OK,
3578 ".*<tr>
3579 <td><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></td>
3580 <td>340282366920938463463374607431768211455\u{A0}%</td>
3581 </tr>.*",
3582 );
3583 }
3584
3585 #[test]
3586 fn transactions_link_to_etching() {
3587 let server = TestServer::builder()
3588 .chain(Chain::Regtest)
3589 .index_runes()
3590 .build();
3591
3592 server.mine_blocks(1);
3593
3594 server.assert_response_regex(
3595 "/runes",
3596 StatusCode::OK,
3597 ".*<title>Runes</title>.*<h1>Runes</h1>\n<ul>\n</ul>.*",
3598 );
3599
3600 let (txid, id) = server.etch(
3601 Runestone {
3602 edicts: vec![Edict {
3603 id: RuneId::default(),
3604 amount: u128::MAX,
3605 output: 0,
3606 }],
3607 etching: Some(Etching {
3608 rune: Some(Rune(RUNE)),
3609 premine: Some(u128::MAX),
3610 ..default()
3611 }),
3612 ..default()
3613 },
3614 1,
3615 None,
3616 );
3617
3618 pretty_assert_eq!(
3619 server.index.runes().unwrap(),
3620 [(
3621 id,
3622 RuneEntry {
3623 block: id.block,
3624 etching: txid,
3625 spaced_rune: SpacedRune {
3626 rune: Rune(RUNE),
3627 spacers: 0
3628 },
3629 premine: u128::MAX,
3630 timestamp: id.block,
3631 ..default()
3632 }
3633 )]
3634 );
3635
3636 pretty_assert_eq!(
3637 server.index.get_rune_balances().unwrap(),
3638 [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
3639 );
3640
3641 server.assert_response_regex(
3642 format!("/tx/{txid}"),
3643 StatusCode::OK,
3644 ".*
3645 <dt>etching</dt>
3646 <dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>
3647.*",
3648 );
3649 }
3650
3651 #[test]
3652 fn runes_are_displayed_on_output_page() {
3653 let server = TestServer::builder()
3654 .chain(Chain::Regtest)
3655 .index_runes()
3656 .build();
3657
3658 server.mine_blocks(1);
3659
3660 let rune = Rune(RUNE);
3661
3662 server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
3663
3664 let (txid, id) = server.etch(
3665 Runestone {
3666 edicts: vec![Edict {
3667 id: RuneId::default(),
3668 amount: u128::MAX,
3669 output: 0,
3670 }],
3671 etching: Some(Etching {
3672 divisibility: Some(1),
3673 rune: Some(rune),
3674 premine: Some(u128::MAX),
3675 ..default()
3676 }),
3677 ..default()
3678 },
3679 1,
3680 None,
3681 );
3682
3683 pretty_assert_eq!(
3684 server.index.runes().unwrap(),
3685 [(
3686 id,
3687 RuneEntry {
3688 block: id.block,
3689 divisibility: 1,
3690 etching: txid,
3691 spaced_rune: SpacedRune { rune, spacers: 0 },
3692 premine: u128::MAX,
3693 timestamp: id.block,
3694 ..default()
3695 }
3696 )]
3697 );
3698
3699 let output = OutPoint { txid, vout: 0 };
3700
3701 assert_eq!(
3702 server.index.get_rune_balances().unwrap(),
3703 [(output, vec![(id, u128::MAX)])]
3704 );
3705
3706 server.assert_response_regex(
3707 format!("/output/{output}"),
3708 StatusCode::OK,
3709 format!(
3710 ".*<title>Output {output}</title>.*<h1>Output <span class=monospace>{output}</span></h1>.*
3711 <dt>runes</dt>
3712 <dd>
3713 <table>
3714 <tr>
3715 <th>rune</th>
3716 <th>balance</th>
3717 </tr>
3718 <tr>
3719 <td><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></td>
3720 <td>34028236692093846346337460743176821145.5\u{A0}¤</td>
3721 </tr>
3722 </table>
3723 </dd>
3724.*"
3725 ),
3726 );
3727
3728 let address = default_address(Chain::Regtest);
3729
3730 pretty_assert_eq!(
3731 server.get_json::<api::Output>(format!("/output/{output}")),
3732 api::Output {
3733 value: 5000000000,
3734 script_pubkey: address.script_pubkey(),
3735 address: Some(uncheck(&address)),
3736 confirmations: 1,
3737 transaction: txid,
3738 sat_ranges: None,
3739 indexed: true,
3740 inscriptions: Some(Vec::new()),
3741 outpoint: output,
3742 runes: Some(
3743 vec![(
3744 SpacedRune {
3745 rune: Rune(RUNE),
3746 spacers: 0
3747 },
3748 Pile {
3749 amount: 340282366920938463463374607431768211455,
3750 divisibility: 1,
3751 symbol: None,
3752 }
3753 )]
3754 .into_iter()
3755 .collect()
3756 ),
3757 spent: false,
3758 }
3759 );
3760 }
3761
3762 #[test]
3763 fn http_to_https_redirect_with_path() {
3764 TestServer::builder()
3765 .redirect_http_to_https()
3766 .https()
3767 .build()
3768 .assert_redirect(
3769 "/sat/0",
3770 &format!("https://{}/sat/0", System::host_name().unwrap()),
3771 );
3772 }
3773
3774 #[test]
3775 fn http_to_https_redirect_with_empty() {
3776 TestServer::builder()
3777 .redirect_http_to_https()
3778 .https()
3779 .build()
3780 .assert_redirect("/", &format!("https://{}/", System::host_name().unwrap()));
3781 }
3782
3783 #[test]
3784 fn status() {
3785 let server = TestServer::builder().chain(Chain::Regtest).build();
3786
3787 server.mine_blocks(3);
3788
3789 server.core.broadcast_tx(TransactionTemplate {
3790 inputs: &[(
3791 1,
3792 0,
3793 0,
3794 inscription("text/plain;charset=utf-8", "hello").to_witness(),
3795 )],
3796 ..default()
3797 });
3798
3799 server.core.broadcast_tx(TransactionTemplate {
3800 inputs: &[(
3801 2,
3802 0,
3803 0,
3804 inscription("text/plain;charset=utf-8", "hello").to_witness(),
3805 )],
3806 ..default()
3807 });
3808
3809 server.core.broadcast_tx(TransactionTemplate {
3810 inputs: &[(
3811 3,
3812 0,
3813 0,
3814 Inscription {
3815 content_type: None,
3816 body: Some("hello".as_bytes().into()),
3817 ..default()
3818 }
3819 .to_witness(),
3820 )],
3821 ..default()
3822 });
3823
3824 server.mine_blocks(1);
3825
3826 server.assert_response_regex(
3827 "/status",
3828 StatusCode::OK,
3829 ".*<h1>Status</h1>
3830<dl>
3831 <dt>chain</dt>
3832 <dd>regtest</dd>
3833 <dt>height</dt>
3834 <dd><a href=/block/4>4</a></dd>
3835 <dt>inscriptions</dt>
3836 <dd><a href=/inscriptions>3</a></dd>
3837 <dt>blessed inscriptions</dt>
3838 <dd>3</dd>
3839 <dt>cursed inscriptions</dt>
3840 <dd>0</dd>
3841 <dt>runes</dt>
3842 <dd><a href=/runes>0</a></dd>
3843 <dt>lost sats</dt>
3844 <dd>.*</dd>
3845 <dt>started</dt>
3846 <dd>.*</dd>
3847 <dt>uptime</dt>
3848 <dd>.*</dd>
3849 <dt>minimum rune for next block</dt>
3850 <dd>.*</dd>
3851 <dt>version</dt>
3852 <dd>.*</dd>
3853 <dt>unrecoverably reorged</dt>
3854 <dd>false</dd>
3855 <dt>address index</dt>
3856 <dd>false</dd>
3857 <dt>inscription index</dt>
3858 <dd>true</dd>
3859 <dt>rune index</dt>
3860 <dd>false</dd>
3861 <dt>sat index</dt>
3862 <dd>false</dd>
3863 <dt>transaction index</dt>
3864 <dd>false</dd>
3865 <dt>json api</dt>
3866 <dd>true</dd>
3867 <dt>git branch</dt>
3868 <dd>.*</dd>
3869 <dt>git commit</dt>
3870 <dd>
3871 <a class=collapse href=https://github.com/ordinals/ord/commit/[[:xdigit:]]{40}>
3872 [[:xdigit:]]{40}
3873 </a>
3874 </dd>
3875</dl>
3876.*",
3877 );
3878 }
3879
3880 #[test]
3881 fn block_count_endpoint() {
3882 let test_server = TestServer::new();
3883
3884 let response = test_server.get("/blockcount");
3885
3886 assert_eq!(response.status(), StatusCode::OK);
3887 assert_eq!(response.text().unwrap(), "1");
3888
3889 test_server.mine_blocks(1);
3890
3891 let response = test_server.get("/blockcount");
3892
3893 assert_eq!(response.status(), StatusCode::OK);
3894 assert_eq!(response.text().unwrap(), "2");
3895 }
3896
3897 #[test]
3898 fn block_height_endpoint() {
3899 let test_server = TestServer::new();
3900
3901 let response = test_server.get("/blockheight");
3902
3903 assert_eq!(response.status(), StatusCode::OK);
3904 assert_eq!(response.text().unwrap(), "0");
3905
3906 test_server.mine_blocks(2);
3907
3908 let response = test_server.get("/blockheight");
3909
3910 assert_eq!(response.status(), StatusCode::OK);
3911 assert_eq!(response.text().unwrap(), "2");
3912 }
3913
3914 #[test]
3915 fn block_hash_endpoint() {
3916 let test_server = TestServer::new();
3917
3918 let response = test_server.get("/blockhash");
3919
3920 assert_eq!(response.status(), StatusCode::OK);
3921 assert_eq!(
3922 response.text().unwrap(),
3923 "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
3924 );
3925 }
3926
3927 #[test]
3928 fn block_hash_from_height_endpoint() {
3929 let test_server = TestServer::new();
3930
3931 let response = test_server.get("/blockhash/0");
3932
3933 assert_eq!(response.status(), StatusCode::OK);
3934 assert_eq!(
3935 response.text().unwrap(),
3936 "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
3937 );
3938 }
3939
3940 #[test]
3941 fn block_time_endpoint() {
3942 let test_server = TestServer::new();
3943
3944 let response = test_server.get("/blocktime");
3945
3946 assert_eq!(response.status(), StatusCode::OK);
3947 assert_eq!(response.text().unwrap(), "1231006505");
3948 }
3949
3950 #[test]
3951 fn sat_number() {
3952 TestServer::new().assert_response_regex("/sat/0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
3953 }
3954
3955 #[test]
3956 fn sat_decimal() {
3957 TestServer::new().assert_response_regex("/sat/0.0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
3958 }
3959
3960 #[test]
3961 fn sat_degree() {
3962 TestServer::new().assert_response_regex("/sat/0°0′0″0‴", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
3963 }
3964
3965 #[test]
3966 fn sat_name() {
3967 TestServer::new().assert_response_regex(
3968 "/sat/nvtdijuwxlp",
3969 StatusCode::OK,
3970 ".*<h1>Sat 0</h1>.*",
3971 );
3972 }
3973
3974 #[test]
3975 fn sat() {
3976 TestServer::new().assert_response_regex(
3977 "/sat/0",
3978 StatusCode::OK,
3979 ".*<title>Sat 0</title>.*<h1>Sat 0</h1>.*",
3980 );
3981 }
3982
3983 #[test]
3984 fn block() {
3985 TestServer::new().assert_response_regex(
3986 "/block/0",
3987 StatusCode::OK,
3988 ".*<title>Block 0</title>.*<h1>Block 0</h1>.*",
3989 );
3990 }
3991
3992 #[test]
3993 fn sat_out_of_range() {
3994 TestServer::new().assert_response(
3995 "/sat/2099999997690000",
3996 StatusCode::BAD_REQUEST,
3997 "Invalid URL: failed to parse sat `2099999997690000`: invalid integer range",
3998 );
3999 }
4000
4001 #[test]
4002 fn invalid_outpoint_hash_returns_400() {
4003 TestServer::new().assert_response(
4004 "/output/foo:0",
4005 StatusCode::BAD_REQUEST,
4006 "Invalid URL: Cannot parse `output` with value `foo:0`: error parsing TXID",
4007 );
4008 }
4009
4010 #[test]
4011 fn output_with_sat_index() {
4012 let txid = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
4013 TestServer::builder()
4014 .index_sats()
4015 .build()
4016 .assert_response_regex(
4017 format!("/output/{txid}:0"),
4018 StatusCode::OK,
4019 format!(
4020 ".*<title>Output {txid}:0</title>.*<h1>Output <span class=monospace>{txid}:0</span></h1>
4021<dl>
4022 <dt>value</dt><dd>5000000000</dd>
4023 <dt>script pubkey</dt><dd class=monospace>OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG</dd>
4024 <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
4025 <dt>confirmations</dt><dd>1</dd>
4026 <dt>spent</dt><dd>false</dd>
4027</dl>
4028<h2>1 Sat Range</h2>
4029<ul class=monospace>
4030 <li><a href=/sat/0 class=mythic>0</a>-<a href=/sat/4999999999 class=common>4999999999</a> \\(5000000000 sats\\)</li>
4031</ul>.*"
4032 ),
4033 );
4034 }
4035
4036 #[test]
4037 fn output_without_sat_index() {
4038 let txid = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
4039 TestServer::new().assert_response_regex(
4040 format!("/output/{txid}:0"),
4041 StatusCode::OK,
4042 format!(
4043 ".*<title>Output {txid}:0</title>.*<h1>Output <span class=monospace>{txid}:0</span></h1>
4044<dl>
4045 <dt>value</dt><dd>5000000000</dd>
4046 <dt>script pubkey</dt><dd class=monospace>OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG</dd>
4047 <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
4048 <dt>confirmations</dt><dd>1</dd>
4049 <dt>spent</dt><dd>false</dd>
4050</dl>.*"
4051 ),
4052 );
4053 }
4054
4055 #[test]
4056 fn null_output_receives_lost_sats() {
4057 let server = TestServer::builder().index_sats().build();
4058
4059 server.mine_blocks_with_subsidy(1, 0);
4060
4061 let txid = "0000000000000000000000000000000000000000000000000000000000000000";
4062
4063 server.assert_response_regex(
4064 format!("/output/{txid}:4294967295"),
4065 StatusCode::OK,
4066 format!(
4067 ".*<title>Output {txid}:4294967295</title>.*<h1>Output <span class=monospace>{txid}:4294967295</span></h1>
4068<dl>
4069 <dt>value</dt><dd>5000000000</dd>
4070 <dt>script pubkey</dt><dd class=monospace></dd>
4071 <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
4072 <dt>confirmations</dt><dd>0</dd>
4073 <dt>spent</dt><dd>false</dd>
4074</dl>
4075<h2>1 Sat Range</h2>
4076<ul class=monospace>
4077 <li><a href=/sat/5000000000 class=uncommon>5000000000</a>-<a href=/sat/9999999999 class=common>9999999999</a> \\(5000000000 sats\\)</li>
4078</ul>.*"
4079 ),
4080 );
4081 }
4082
4083 #[test]
4084 fn unbound_output_receives_unbound_inscriptions() {
4085 let server = TestServer::builder()
4086 .chain(Chain::Regtest)
4087 .index_sats()
4088 .build();
4089
4090 server.mine_blocks(1);
4091
4092 server.core.broadcast_tx(TransactionTemplate {
4093 inputs: &[(1, 0, 0, Default::default())],
4094 fee: 50 * 100_000_000,
4095 ..default()
4096 });
4097
4098 server.mine_blocks(1);
4099
4100 let txid = server.core.broadcast_tx(TransactionTemplate {
4101 inputs: &[(
4102 2,
4103 1,
4104 0,
4105 inscription("text/plain;charset=utf-8", "hello").to_witness(),
4106 )],
4107 ..default()
4108 });
4109
4110 server.mine_blocks(1);
4111
4112 let inscription_id = InscriptionId { txid, index: 0 };
4113
4114 server.assert_response_regex(
4115 format!("/inscription/{inscription_id}"),
4116 StatusCode::OK,
4117 format!(
4118 ".*<dl>
4119 <dt>id</dt>
4120 <dd class=collapse>{inscription_id}</dd>.*<dt>output</dt>
4121 <dd><a class=collapse href=/output/0000000000000000000000000000000000000000000000000000000000000000:0>0000000000000000000000000000000000000000000000000000000000000000:0</a></dd>.*"
4122 ),
4123 );
4124
4125 server.assert_response_regex(
4126 "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
4127 StatusCode::OK,
4128 ".*<h1>Output <span class=monospace>0000000000000000000000000000000000000000000000000000000000000000:0</span></h1>
4129<dl>
4130 <dt>inscriptions</dt>
4131 <dd class=thumbnails>
4132 <a href=/inscription/.*><iframe sandbox=allow-scripts scrolling=no loading=lazy src=/preview/.*></iframe></a>
4133 </dd>.*",
4134 );
4135 }
4136
4137 #[test]
4138 fn unbound_output_returns_200() {
4139 TestServer::new().assert_response_regex(
4140 "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
4141 StatusCode::OK,
4142 ".*",
4143 );
4144 }
4145
4146 #[test]
4147 fn invalid_output_returns_400() {
4148 TestServer::new().assert_response(
4149 "/output/foo:0",
4150 StatusCode::BAD_REQUEST,
4151 "Invalid URL: Cannot parse `output` with value `foo:0`: error parsing TXID",
4152 );
4153 }
4154
4155 #[test]
4156 fn home() {
4157 let server = TestServer::builder().chain(Chain::Regtest).build();
4158
4159 server.mine_blocks(1);
4160
4161 let mut ids = Vec::new();
4162
4163 for i in 0..101 {
4164 let txid = server.core.broadcast_tx(TransactionTemplate {
4165 inputs: &[(i + 1, 0, 0, inscription("image/png", "hello").to_witness())],
4166 ..default()
4167 });
4168 ids.push(InscriptionId { txid, index: 0 });
4169 server.mine_blocks(1);
4170 }
4171
4172 server.core.broadcast_tx(TransactionTemplate {
4173 inputs: &[(102, 0, 0, inscription("text/plain", "{}").to_witness())],
4174 ..default()
4175 });
4176
4177 server.mine_blocks(1);
4178
4179 server.assert_response_regex(
4180 "/",
4181 StatusCode::OK,
4182 format!(
4183 r".*<title>Ordinals</title>.*
4184<h1>Latest Inscriptions</h1>
4185<div class=thumbnails>
4186 <a href=/inscription/{}>.*</a>
4187 (<a href=/inscription/[[:xdigit:]]{{64}}i0>.*</a>\s*){{99}}
4188</div>
4189.*
4190",
4191 ids[100]
4192 ),
4193 );
4194 }
4195
4196 #[test]
4197 fn blocks() {
4198 let test_server = TestServer::new();
4199
4200 test_server.mine_blocks(1);
4201
4202 test_server.assert_response_regex(
4203 "/blocks",
4204 StatusCode::OK,
4205 ".*<title>Blocks</title>.*
4206<h1>Blocks</h1>
4207<div class=block>
4208 <h2><a href=/block/1>Block 1</a></h2>
4209 <div class=thumbnails>
4210 </div>
4211</div>
4212<div class=block>
4213 <h2><a href=/block/0>Block 0</a></h2>
4214 <div class=thumbnails>
4215 </div>
4216</div>
4217</ol>.*",
4218 );
4219 }
4220
4221 #[test]
4222 fn nav_displays_chain() {
4223 TestServer::builder()
4224 .chain(Chain::Regtest)
4225 .build()
4226 .assert_response_regex(
4227 "/",
4228 StatusCode::OK,
4229 ".*<a href=/ title=home>Ordinals<sup>regtest</sup></a>.*",
4230 );
4231 }
4232
4233 #[test]
4234 fn blocks_block_limit() {
4235 let test_server = TestServer::new();
4236
4237 test_server.mine_blocks(101);
4238
4239 test_server.assert_response_regex(
4240 "/blocks",
4241 StatusCode::OK,
4242 ".*<ol start=96 reversed class=block-list>\n( <li><a class=collapse href=/block/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>\n){95}</ol>.*"
4243 );
4244 }
4245
4246 #[test]
4247 fn block_not_found() {
4248 TestServer::new().assert_response(
4249 "/block/467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16",
4250 StatusCode::NOT_FOUND,
4251 "block 467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16 not found",
4252 );
4253 }
4254
4255 #[test]
4256 fn unmined_sat() {
4257 TestServer::new().assert_response_regex(
4258 "/sat/0",
4259 StatusCode::OK,
4260 ".*<dt>timestamp</dt><dd><time>2009-01-03 18:15:05 UTC</time></dd>.*",
4261 );
4262 }
4263
4264 #[test]
4265 fn mined_sat() {
4266 TestServer::new().assert_response_regex(
4267 "/sat/5000000000",
4268 StatusCode::OK,
4269 ".*<dt>timestamp</dt><dd><time>.*</time> \\(expected\\)</dd>.*",
4270 );
4271 }
4272
4273 #[test]
4274 fn static_asset() {
4275 TestServer::new().assert_response_regex(
4276 "/static/index.css",
4277 StatusCode::OK,
4278 r".*\.rare \{
4279 background-color: var\(--rare\);
4280}.*",
4281 );
4282 }
4283
4284 #[test]
4285 fn favicon() {
4286 TestServer::new().assert_response_regex("/favicon.ico", StatusCode::OK, r".*");
4287 }
4288
4289 #[test]
4290 fn clock_updates() {
4291 let test_server = TestServer::new();
4292 test_server.assert_response_regex("/clock", StatusCode::OK, ".*<text.*>0</text>.*");
4293 test_server.mine_blocks(1);
4294 test_server.assert_response_regex("/clock", StatusCode::OK, ".*<text.*>1</text>.*");
4295 }
4296
4297 #[test]
4298 fn block_by_hash() {
4299 let test_server = TestServer::new();
4300
4301 test_server.mine_blocks(1);
4302 let transaction = TransactionTemplate {
4303 inputs: &[(1, 0, 0, Default::default())],
4304 fee: 0,
4305 ..default()
4306 };
4307 test_server.core.broadcast_tx(transaction);
4308 let block_hash = test_server.mine_blocks(1)[0].block_hash();
4309
4310 test_server.assert_response_regex(
4311 format!("/block/{block_hash}"),
4312 StatusCode::OK,
4313 ".*<h1>Block 2</h1>.*",
4314 );
4315 }
4316
4317 #[test]
4318 fn block_by_height() {
4319 let test_server = TestServer::new();
4320
4321 test_server.assert_response_regex("/block/0", StatusCode::OK, ".*<h1>Block 0</h1>.*");
4322 }
4323
4324 #[test]
4325 fn transaction() {
4326 let test_server = TestServer::new();
4327
4328 let coinbase_tx = test_server.mine_blocks(1)[0].txdata[0].clone();
4329 let txid = coinbase_tx.compute_txid();
4330
4331 test_server.assert_response_regex(
4332 format!("/tx/{txid}"),
4333 StatusCode::OK,
4334 format!(
4335 ".*<title>Transaction {txid}</title>.*<h1>Transaction <span class=monospace>{txid}</span></h1>
4336<dl>
4337</dl>
4338<h2>1 Input</h2>
4339<ul>
4340 <li><a class=collapse href=/output/0000000000000000000000000000000000000000000000000000000000000000:4294967295>0000000000000000000000000000000000000000000000000000000000000000:4294967295</a></li>
4341</ul>
4342<h2>1 Output</h2>
4343<ul class=monospace>
4344 <li>
4345 <a href=/output/{txid}:0 class=collapse>
4346 {txid}:0
4347 </a>
4348 <dl>
4349 <dt>value</dt><dd>5000000000</dd>
4350 <dt>script pubkey</dt><dd class=monospace>.*</dd>
4351 </dl>
4352 </li>
4353</ul>.*"
4354 ),
4355 );
4356 }
4357
4358 #[test]
4359 fn recursive_transaction_hex_endpoint() {
4360 let test_server = TestServer::new();
4361
4362 let coinbase_tx = test_server.mine_blocks(1)[0].txdata[0].clone();
4363 let txid = coinbase_tx.compute_txid();
4364
4365 test_server.assert_response(
4366 format!("/r/tx/{txid}"),
4367 StatusCode::OK,
4368 "\"02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0151ffffffff0100f2052a01000000225120be7cbbe9ca06a7d7b2a17c6b4ff4b85b362cbcd7ee1970daa66dfaa834df59a000000000\""
4369 );
4370 }
4371
4372 #[test]
4373 fn recursive_transaction_hex_endpoint_for_genesis_transaction() {
4374 let test_server = TestServer::new();
4375
4376 test_server.mine_blocks(1);
4377
4378 test_server.assert_response(
4379 "/r/tx/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
4380 StatusCode::OK,
4381 "\"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000\""
4382 );
4383 }
4384
4385 #[test]
4386 fn detect_unrecoverable_reorg() {
4387 let test_server = TestServer::new();
4388
4389 test_server.mine_blocks(21);
4390
4391 test_server.assert_response_regex(
4392 "/status",
4393 StatusCode::OK,
4394 ".*<dt>unrecoverably reorged</dt>\n <dd>false</dd>.*",
4395 );
4396
4397 for _ in 0..15 {
4398 test_server.core.invalidate_tip();
4399 }
4400
4401 test_server.core.mine_blocks(21);
4402
4403 test_server.assert_response_regex(
4404 "/status",
4405 StatusCode::OK,
4406 ".*<dt>unrecoverably reorged</dt>\n <dd>true</dd>.*",
4407 );
4408 }
4409
4410 #[test]
4411 fn rare_with_sat_index() {
4412 TestServer::builder().index_sats().build().assert_response(
4413 "/rare.txt",
4414 StatusCode::OK,
4415 "sat\tsatpoint
44160\t4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0
4417",
4418 );
4419 }
4420
4421 #[test]
4422 fn rare_without_sat_index() {
4423 TestServer::new().assert_response(
4424 "/rare.txt",
4425 StatusCode::OK,
4426 "sat\tsatpoint
4427",
4428 );
4429 }
4430
4431 #[test]
4432 fn show_rare_txt_in_header_with_sat_index() {
4433 TestServer::builder()
4434 .index_sats()
4435 .build()
4436 .assert_response_regex(
4437 "/",
4438 StatusCode::OK,
4439 ".*
4440 <a href=/clock title=clock>.*</a>
4441 <a href=/rare.txt title=rare>.*</a>.*",
4442 );
4443 }
4444
4445 #[test]
4446 fn rare_sat_location() {
4447 TestServer::builder()
4448 .index_sats()
4449 .build()
4450 .assert_response_regex(
4451 "/sat/0",
4452 StatusCode::OK,
4453 ".*>4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0<.*",
4454 );
4455 }
4456
4457 #[test]
4458 fn dont_show_rare_txt_in_header_without_sat_index() {
4459 TestServer::new().assert_response_regex(
4460 "/",
4461 StatusCode::OK,
4462 ".*
4463 <a href=/clock title=clock>.*</a>
4464 <a href=https://docs.ordinals.com/.*",
4465 );
4466 }
4467
4468 #[test]
4469 fn input() {
4470 TestServer::new().assert_response_regex(
4471 "/input/0/0/0",
4472 StatusCode::OK,
4473 ".*<title>Input /0/0/0</title>.*<h1>Input /0/0/0</h1>.*<dt>text</dt><dd>.*The Times 03/Jan/2009 Chancellor on brink of second bailout for banks</dd>.*",
4474 );
4475 }
4476
4477 #[test]
4478 fn input_missing() {
4479 TestServer::new().assert_response(
4480 "/input/1/1/1",
4481 StatusCode::NOT_FOUND,
4482 "input /1/1/1 not found",
4483 );
4484 }
4485
4486 #[test]
4487 fn commits_are_tracked() {
4488 let server = TestServer::new();
4489
4490 thread::sleep(Duration::from_millis(100));
4491 assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1);
4492
4493 let info = server.index.info().unwrap();
4494 assert_eq!(info.transactions.len(), 1);
4495 assert_eq!(info.transactions[0].starting_block_count, 0);
4496
4497 server.index.update().unwrap();
4498
4499 assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1);
4500
4501 let info = server.index.info().unwrap();
4502 assert_eq!(info.transactions.len(), 1);
4503 assert_eq!(info.transactions[0].starting_block_count, 0);
4504
4505 server.mine_blocks(1);
4506
4507 thread::sleep(Duration::from_millis(10));
4508 server.index.update().unwrap();
4509
4510 assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 2);
4511
4512 let info = server.index.info().unwrap();
4513 assert_eq!(info.transactions.len(), 2);
4514 assert_eq!(info.transactions[0].starting_block_count, 0);
4515 assert_eq!(info.transactions[1].starting_block_count, 1);
4516 assert!(
4517 info.transactions[1].starting_timestamp - info.transactions[0].starting_timestamp >= 10
4518 );
4519 }
4520
4521 #[test]
4522 fn outputs_traversed_are_tracked() {
4523 let server = TestServer::builder().index_sats().build();
4524
4525 assert_eq!(
4526 server
4527 .index
4528 .statistic(crate::index::Statistic::OutputsTraversed),
4529 1
4530 );
4531
4532 server.index.update().unwrap();
4533
4534 assert_eq!(
4535 server
4536 .index
4537 .statistic(crate::index::Statistic::OutputsTraversed),
4538 1
4539 );
4540
4541 server.mine_blocks(2);
4542
4543 server.index.update().unwrap();
4544
4545 assert_eq!(
4546 server
4547 .index
4548 .statistic(crate::index::Statistic::OutputsTraversed),
4549 3
4550 );
4551 }
4552
4553 #[test]
4554 fn coinbase_sat_ranges_are_tracked() {
4555 let server = TestServer::builder().index_sats().build();
4556
4557 assert_eq!(
4558 server.index.statistic(crate::index::Statistic::SatRanges),
4559 1
4560 );
4561
4562 server.mine_blocks(1);
4563
4564 assert_eq!(
4565 server.index.statistic(crate::index::Statistic::SatRanges),
4566 2
4567 );
4568
4569 server.mine_blocks(1);
4570
4571 assert_eq!(
4572 server.index.statistic(crate::index::Statistic::SatRanges),
4573 3
4574 );
4575 }
4576
4577 #[test]
4578 fn split_sat_ranges_are_tracked() {
4579 let server = TestServer::builder().index_sats().build();
4580
4581 assert_eq!(
4582 server.index.statistic(crate::index::Statistic::SatRanges),
4583 1
4584 );
4585
4586 server.mine_blocks(1);
4587 server.core.broadcast_tx(TransactionTemplate {
4588 inputs: &[(1, 0, 0, Default::default())],
4589 outputs: 2,
4590 fee: 0,
4591 ..default()
4592 });
4593 server.mine_blocks(1);
4594
4595 assert_eq!(
4596 server.index.statistic(crate::index::Statistic::SatRanges),
4597 4,
4598 );
4599 }
4600
4601 #[test]
4602 fn fee_sat_ranges_are_tracked() {
4603 let server = TestServer::builder().index_sats().build();
4604
4605 assert_eq!(
4606 server.index.statistic(crate::index::Statistic::SatRanges),
4607 1
4608 );
4609
4610 server.mine_blocks(1);
4611 server.core.broadcast_tx(TransactionTemplate {
4612 inputs: &[(1, 0, 0, Default::default())],
4613 outputs: 2,
4614 fee: 2,
4615 ..default()
4616 });
4617 server.mine_blocks(1);
4618
4619 assert_eq!(
4620 server.index.statistic(crate::index::Statistic::SatRanges),
4621 5,
4622 );
4623 }
4624
4625 #[test]
4626 fn content_response_no_content() {
4627 assert_eq!(
4628 r::content_response(
4629 Inscription {
4630 content_type: Some("text/plain".as_bytes().to_vec()),
4631 body: None,
4632 ..default()
4633 },
4634 AcceptEncoding::default(),
4635 &ServerConfig::default(),
4636 true,
4637 )
4638 .unwrap(),
4639 None
4640 );
4641 }
4642
4643 #[test]
4644 fn content_response_with_content() {
4645 let (headers, body) = r::content_response(
4646 Inscription {
4647 content_type: Some("text/plain".as_bytes().to_vec()),
4648 body: Some(vec![1, 2, 3]),
4649 ..default()
4650 },
4651 AcceptEncoding::default(),
4652 &ServerConfig::default(),
4653 true,
4654 )
4655 .unwrap()
4656 .unwrap();
4657
4658 assert_eq!(headers["content-type"], "text/plain");
4659 assert_eq!(body, vec![1, 2, 3]);
4660 }
4661
4662 #[test]
4663 fn content_security_policy_no_origin() {
4664 let (headers, _) = r::content_response(
4665 Inscription {
4666 content_type: Some("text/plain".as_bytes().to_vec()),
4667 body: Some(vec![1, 2, 3]),
4668 ..default()
4669 },
4670 AcceptEncoding::default(),
4671 &ServerConfig::default(),
4672 true,
4673 )
4674 .unwrap()
4675 .unwrap();
4676
4677 assert_eq!(
4678 headers["content-security-policy"],
4679 HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:")
4680 );
4681 }
4682
4683 #[test]
4684 fn content_security_policy_with_origin() {
4685 let (headers, _) = r::content_response(
4686 Inscription {
4687 content_type: Some("text/plain".as_bytes().to_vec()),
4688 body: Some(vec![1, 2, 3]),
4689 ..default()
4690 },
4691 AcceptEncoding::default(),
4692 &ServerConfig {
4693 csp_origin: Some("https://ordinals.com".into()),
4694 ..default()
4695 },
4696 true,
4697 )
4698 .unwrap()
4699 .unwrap();
4700
4701 assert_eq!(
4702 headers["content-security-policy"],
4703 HeaderValue::from_static(
4704 "default-src https://ordinals.com/content/ https://ordinals.com/blockheight https://ordinals.com/blockhash https://ordinals.com/blockhash/ https://ordinals.com/blocktime https://ordinals.com/r/ 'unsafe-eval' 'unsafe-inline' data: blob:"
4705 )
4706 );
4707 }
4708
4709 #[test]
4710 fn preview_content_security_policy() {
4711 {
4712 let server = TestServer::builder().chain(Chain::Regtest).build();
4713
4714 server.mine_blocks(1);
4715
4716 let txid = server.core.broadcast_tx(TransactionTemplate {
4717 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
4718 ..default()
4719 });
4720
4721 server.mine_blocks(1);
4722
4723 let inscription_id = InscriptionId { txid, index: 0 };
4724
4725 server.assert_response_csp(
4726 format!("/preview/{inscription_id}"),
4727 StatusCode::OK,
4728 "default-src 'self'",
4729 format!(
4730 ".*<html lang=en data-inscription={inscription_id}>.*<title>Inscription 0 Preview</title>.*"
4731 ),
4732 );
4733 }
4734
4735 {
4736 let server = TestServer::builder()
4737 .chain(Chain::Regtest)
4738 .server_option("--csp-origin", "https://ordinals.com")
4739 .build();
4740
4741 server.mine_blocks(1);
4742
4743 let txid = server.core.broadcast_tx(TransactionTemplate {
4744 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
4745 ..default()
4746 });
4747
4748 server.mine_blocks(1);
4749
4750 let inscription_id = InscriptionId { txid, index: 0 };
4751
4752 server.assert_response_csp(
4753 format!("/preview/{inscription_id}"),
4754 StatusCode::OK,
4755 "default-src https://ordinals.com",
4756 format!(".*<html lang=en data-inscription={inscription_id}>.*"),
4757 );
4758 }
4759 }
4760
4761 #[test]
4762 fn code_preview() {
4763 let server = TestServer::builder().chain(Chain::Regtest).build();
4764 server.mine_blocks(1);
4765
4766 let txid = server.core.broadcast_tx(TransactionTemplate {
4767 inputs: &[(
4768 1,
4769 0,
4770 0,
4771 inscription("text/javascript", "hello").to_witness(),
4772 )],
4773 ..default()
4774 });
4775 let inscription_id = InscriptionId { txid, index: 0 };
4776
4777 server.mine_blocks(1);
4778
4779 server.assert_response_regex(
4780 format!("/preview/{inscription_id}"),
4781 StatusCode::OK,
4782 format!(r".*<html lang=en data-inscription={inscription_id} data-language=javascript>.*"),
4783 );
4784 }
4785
4786 #[test]
4787 fn content_response_no_content_type() {
4788 let (headers, body) = r::content_response(
4789 Inscription {
4790 content_type: None,
4791 body: Some(Vec::new()),
4792 ..default()
4793 },
4794 AcceptEncoding::default(),
4795 &ServerConfig::default(),
4796 true,
4797 )
4798 .unwrap()
4799 .unwrap();
4800
4801 assert_eq!(headers["content-type"], "application/octet-stream");
4802 assert!(body.is_empty());
4803 }
4804
4805 #[test]
4806 fn content_response_bad_content_type() {
4807 let (headers, body) = r::content_response(
4808 Inscription {
4809 content_type: Some("\n".as_bytes().to_vec()),
4810 body: Some(Vec::new()),
4811 ..Default::default()
4812 },
4813 AcceptEncoding::default(),
4814 &ServerConfig::default(),
4815 true,
4816 )
4817 .unwrap()
4818 .unwrap();
4819
4820 assert_eq!(headers["content-type"], "application/octet-stream");
4821 assert!(body.is_empty());
4822 }
4823
4824 #[test]
4825 fn text_preview() {
4826 let server = TestServer::builder().chain(Chain::Regtest).build();
4827 server.mine_blocks(1);
4828
4829 let txid = server.core.broadcast_tx(TransactionTemplate {
4830 inputs: &[(
4831 1,
4832 0,
4833 0,
4834 inscription("text/plain;charset=utf-8", "hello").to_witness(),
4835 )],
4836 ..default()
4837 });
4838
4839 let inscription_id = InscriptionId { txid, index: 0 };
4840
4841 server.mine_blocks(1);
4842
4843 server.assert_response_csp(
4844 format!("/preview/{inscription_id}"),
4845 StatusCode::OK,
4846 "default-src 'self'",
4847 format!(".*<html lang=en data-inscription={inscription_id}>.*"),
4848 );
4849 }
4850
4851 #[test]
4852 fn audio_preview() {
4853 let server = TestServer::builder().chain(Chain::Regtest).build();
4854 server.mine_blocks(1);
4855
4856 let txid = server.core.broadcast_tx(TransactionTemplate {
4857 inputs: &[(1, 0, 0, inscription("audio/flac", "hello").to_witness())],
4858 ..default()
4859 });
4860 let inscription_id = InscriptionId { txid, index: 0 };
4861
4862 server.mine_blocks(1);
4863
4864 server.assert_response_regex(
4865 format!("/preview/{inscription_id}"),
4866 StatusCode::OK,
4867 format!(r".*<audio .*>\s*<source src=/content/{inscription_id}>.*"),
4868 );
4869 }
4870
4871 #[test]
4872 fn font_preview() {
4873 let server = TestServer::builder().chain(Chain::Regtest).build();
4874 server.mine_blocks(1);
4875
4876 let txid = server.core.broadcast_tx(TransactionTemplate {
4877 inputs: &[(1, 0, 0, inscription("font/ttf", "hello").to_witness())],
4878 ..default()
4879 });
4880 let inscription_id = InscriptionId { txid, index: 0 };
4881
4882 server.mine_blocks(1);
4883
4884 server.assert_response_regex(
4885 format!("/preview/{inscription_id}"),
4886 StatusCode::OK,
4887 format!(r".*src: url\(/content/{inscription_id}\).*"),
4888 );
4889 }
4890
4891 #[test]
4892 fn pdf_preview() {
4893 let server = TestServer::builder().chain(Chain::Regtest).build();
4894 server.mine_blocks(1);
4895
4896 let txid = server.core.broadcast_tx(TransactionTemplate {
4897 inputs: &[(
4898 1,
4899 0,
4900 0,
4901 inscription("application/pdf", "hello").to_witness(),
4902 )],
4903 ..default()
4904 });
4905 let inscription_id = InscriptionId { txid, index: 0 };
4906
4907 server.mine_blocks(1);
4908
4909 server.assert_response_regex(
4910 format!("/preview/{inscription_id}"),
4911 StatusCode::OK,
4912 format!(r".*<canvas data-inscription={inscription_id}></canvas>.*"),
4913 );
4914 }
4915
4916 #[test]
4917 fn markdown_preview() {
4918 let server = TestServer::builder().chain(Chain::Regtest).build();
4919 server.mine_blocks(1);
4920
4921 let txid = server.core.broadcast_tx(TransactionTemplate {
4922 inputs: &[(1, 0, 0, inscription("text/markdown", "hello").to_witness())],
4923 ..default()
4924 });
4925 let inscription_id = InscriptionId { txid, index: 0 };
4926
4927 server.mine_blocks(1);
4928
4929 server.assert_response_regex(
4930 format!("/preview/{inscription_id}"),
4931 StatusCode::OK,
4932 format!(r".*<html lang=en data-inscription={inscription_id}>.*"),
4933 );
4934 }
4935
4936 #[test]
4937 fn image_preview() {
4938 let server = TestServer::builder().chain(Chain::Regtest).build();
4939 server.mine_blocks(1);
4940
4941 let txid = server.core.broadcast_tx(TransactionTemplate {
4942 inputs: &[(1, 0, 0, inscription("image/png", "hello").to_witness())],
4943 ..default()
4944 });
4945 let inscription_id = InscriptionId { txid, index: 0 };
4946
4947 server.mine_blocks(1);
4948
4949 server.assert_response_csp(
4950 format!("/preview/{inscription_id}"),
4951 StatusCode::OK,
4952 "default-src 'self' 'unsafe-inline'",
4953 format!(r".*background-image: url\(/content/{inscription_id}\);.*"),
4954 );
4955 }
4956
4957 #[test]
4958 fn iframe_preview() {
4959 let server = TestServer::builder().chain(Chain::Regtest).build();
4960 server.mine_blocks(1);
4961
4962 let txid = server.core.broadcast_tx(TransactionTemplate {
4963 inputs: &[(
4964 1,
4965 0,
4966 0,
4967 inscription("text/html;charset=utf-8", "hello").to_witness(),
4968 )],
4969 ..default()
4970 });
4971
4972 server.mine_blocks(1);
4973
4974 server.assert_response_csp(
4975 format!("/preview/{}", InscriptionId { txid, index: 0 }),
4976 StatusCode::OK,
4977 "default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:",
4978 "hello",
4979 );
4980 }
4981
4982 #[test]
4983 fn unknown_preview() {
4984 let server = TestServer::builder().chain(Chain::Regtest).build();
4985 server.mine_blocks(1);
4986
4987 let txid = server.core.broadcast_tx(TransactionTemplate {
4988 inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
4989 ..default()
4990 });
4991
4992 server.mine_blocks(1);
4993
4994 server.assert_response_csp(
4995 format!("/preview/{}", InscriptionId { txid, index: 0 }),
4996 StatusCode::OK,
4997 "default-src 'self'",
4998 fs::read_to_string("templates/preview-unknown.html").unwrap(),
4999 );
5000 }
5001
5002 #[test]
5003 fn video_preview() {
5004 let server = TestServer::builder().chain(Chain::Regtest).build();
5005 server.mine_blocks(1);
5006
5007 let txid = server.core.broadcast_tx(TransactionTemplate {
5008 inputs: &[(1, 0, 0, inscription("video/webm", "hello").to_witness())],
5009 ..default()
5010 });
5011 let inscription_id = InscriptionId { txid, index: 0 };
5012
5013 server.mine_blocks(1);
5014
5015 server.assert_response_regex(
5016 format!("/preview/{inscription_id}"),
5017 StatusCode::OK,
5018 format!(r".*<video .*>\s*<source src=/content/{inscription_id}>.*"),
5019 );
5020 }
5021
5022 #[test]
5023 fn inscription_page_title() {
5024 let server = TestServer::builder()
5025 .chain(Chain::Regtest)
5026 .index_sats()
5027 .build();
5028 server.mine_blocks(1);
5029
5030 let txid = server.core.broadcast_tx(TransactionTemplate {
5031 inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
5032 ..default()
5033 });
5034
5035 server.mine_blocks(1);
5036
5037 server.assert_response_regex(
5038 format!("/inscription/{}", InscriptionId { txid, index: 0 }),
5039 StatusCode::OK,
5040 ".*<title>Inscription 0</title>.*",
5041 );
5042 }
5043
5044 #[test]
5045 fn inscription_page_has_sat_when_sats_are_tracked() {
5046 let server = TestServer::builder()
5047 .chain(Chain::Regtest)
5048 .index_sats()
5049 .build();
5050 server.mine_blocks(1);
5051
5052 let txid = server.core.broadcast_tx(TransactionTemplate {
5053 inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
5054 ..default()
5055 });
5056
5057 server.mine_blocks(1);
5058
5059 server.assert_response_regex(
5060 format!("/inscription/{}", InscriptionId { txid, index: 0 }),
5061 StatusCode::OK,
5062 r".*<dt>sat</dt>\s*<dd><a href=/sat/5000000000>5000000000</a></dd>\s*<dt>sat name</dt>\s*<dd><a href=/sat/nvtcsezkbth>nvtcsezkbth</a></dd>\s*<dt>preview</dt>.*",
5063 );
5064 }
5065
5066 #[test]
5067 fn inscriptions_can_be_looked_up_by_sat_name() {
5068 let server = TestServer::builder()
5069 .chain(Chain::Regtest)
5070 .index_sats()
5071 .build();
5072 server.mine_blocks(1);
5073
5074 server.core.broadcast_tx(TransactionTemplate {
5075 inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
5076 ..default()
5077 });
5078
5079 server.mine_blocks(1);
5080
5081 server.assert_response_regex(
5082 format!("/inscription/{}", Sat(5000000000).name()),
5083 StatusCode::OK,
5084 ".*<title>Inscription 0</title.*",
5085 );
5086 }
5087
5088 #[test]
5089 fn gallery_items_can_be_looked_up_by_gallery_sat_name() {
5090 let server = TestServer::builder()
5091 .chain(Chain::Regtest)
5092 .index_sats()
5093 .build();
5094
5095 server.mine_blocks(1);
5096
5097 server.core.broadcast_tx(TransactionTemplate {
5098 inputs: &[(
5099 1,
5100 0,
5101 0,
5102 Inscription {
5103 content_type: Some("test/foo".into()),
5104 body: Some("foo".into()),
5105 properties: Properties {
5106 gallery: vec![Item {
5107 id: Some(inscription_id(1)),
5108 ..default()
5109 }],
5110 ..default()
5111 }
5112 .to_inline_cbor(),
5113 ..default()
5114 }
5115 .to_witness(),
5116 )],
5117 ..default()
5118 });
5119
5120 server.mine_blocks(1);
5121
5122 server.assert_response_regex(
5123 format!("/gallery/{}/0", Sat(5000000000).name()),
5124 StatusCode::OK,
5125 ".*<title>Gallery 0 Item 0</title.*",
5126 );
5127 }
5128
5129 #[test]
5130 fn inscriptions_can_be_looked_up_by_sat_name_with_letter_i() {
5131 let server = TestServer::builder()
5132 .chain(Chain::Regtest)
5133 .index_sats()
5134 .build();
5135 server.assert_response_regex("/inscription/i", StatusCode::NOT_FOUND, ".*");
5136 }
5137
5138 #[test]
5139 fn inscription_page_does_not_have_sat_when_sats_are_not_tracked() {
5140 let server = TestServer::builder().chain(Chain::Regtest).build();
5141 server.mine_blocks(1);
5142
5143 let txid = server.core.broadcast_tx(TransactionTemplate {
5144 inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
5145 ..default()
5146 });
5147
5148 server.mine_blocks(1);
5149
5150 server.assert_response_regex(
5151 format!("/inscription/{}", InscriptionId { txid, index: 0 }),
5152 StatusCode::OK,
5153 r".*<dt>value</dt>\s*<dd>5000000000</dd>\s*<dt>preview</dt>.*",
5154 );
5155 }
5156
5157 #[test]
5158 fn strict_transport_security_header_is_set() {
5159 assert_eq!(
5160 TestServer::new()
5161 .get("/status")
5162 .headers()
5163 .get(header::STRICT_TRANSPORT_SECURITY)
5164 .unwrap(),
5165 "max-age=31536000; includeSubDomains; preload",
5166 );
5167 }
5168
5169 #[test]
5170 fn feed() {
5171 let server = TestServer::builder()
5172 .chain(Chain::Regtest)
5173 .index_sats()
5174 .build();
5175 server.mine_blocks(1);
5176
5177 server.core.broadcast_tx(TransactionTemplate {
5178 inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
5179 ..default()
5180 });
5181
5182 server.mine_blocks(1);
5183
5184 server.assert_response_regex(
5185 "/feed.xml",
5186 StatusCode::OK,
5187 ".*<title>Inscription 0</title>.*",
5188 );
5189 }
5190
5191 #[test]
5192 fn inscription_with_unknown_type_and_no_body_has_unknown_preview() {
5193 let server = TestServer::builder()
5194 .chain(Chain::Regtest)
5195 .index_sats()
5196 .build();
5197 server.mine_blocks(1);
5198
5199 let txid = server.core.broadcast_tx(TransactionTemplate {
5200 inputs: &[(
5201 1,
5202 0,
5203 0,
5204 Inscription {
5205 content_type: Some("foo/bar".as_bytes().to_vec()),
5206 body: None,
5207 ..default()
5208 }
5209 .to_witness(),
5210 )],
5211 ..default()
5212 });
5213
5214 let inscription_id = InscriptionId { txid, index: 0 };
5215
5216 server.mine_blocks(1);
5217
5218 server.assert_response(
5219 format!("/preview/{inscription_id}"),
5220 StatusCode::OK,
5221 &fs::read_to_string("templates/preview-unknown.html").unwrap(),
5222 );
5223 }
5224
5225 #[test]
5226 fn inscription_with_known_type_and_no_body_has_unknown_preview() {
5227 let server = TestServer::builder()
5228 .chain(Chain::Regtest)
5229 .index_sats()
5230 .build();
5231 server.mine_blocks(1);
5232
5233 let txid = server.core.broadcast_tx(TransactionTemplate {
5234 inputs: &[(
5235 1,
5236 0,
5237 0,
5238 Inscription {
5239 content_type: Some("image/png".as_bytes().to_vec()),
5240 body: None,
5241 ..default()
5242 }
5243 .to_witness(),
5244 )],
5245 ..default()
5246 });
5247
5248 let inscription_id = InscriptionId { txid, index: 0 };
5249
5250 server.mine_blocks(1);
5251
5252 server.assert_response(
5253 format!("/preview/{inscription_id}"),
5254 StatusCode::OK,
5255 &fs::read_to_string("templates/preview-unknown.html").unwrap(),
5256 );
5257 }
5258
5259 #[test]
5260 fn content_responses_have_cache_control_headers() {
5261 let server = TestServer::builder().chain(Chain::Regtest).build();
5262 server.mine_blocks(1);
5263
5264 let txid = server.core.broadcast_tx(TransactionTemplate {
5265 inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
5266 ..default()
5267 });
5268
5269 server.mine_blocks(1);
5270
5271 let response = server.get(format!("/content/{}", InscriptionId { txid, index: 0 }));
5272
5273 assert_eq!(response.status(), StatusCode::OK);
5274 assert_eq!(
5275 response.headers().get(header::CACHE_CONTROL).unwrap(),
5276 "public, max-age=1209600, immutable"
5277 );
5278 }
5279
5280 #[test]
5281 fn error_content_responses_have_max_age_zero_cache_control_headers() {
5282 let server = TestServer::builder().chain(Chain::Regtest).build();
5283 let response =
5284 server.get("/content/6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0");
5285
5286 assert_eq!(response.status(), 404);
5287 assert_eq!(
5288 response.headers().get(header::CACHE_CONTROL).unwrap(),
5289 "no-store"
5290 );
5291 }
5292
5293 #[test]
5294 fn inscriptions_page_with_no_prev_or_next() {
5295 TestServer::builder()
5296 .chain(Chain::Regtest)
5297 .index_sats()
5298 .build()
5299 .assert_response_regex("/inscriptions", StatusCode::OK, ".*prev\nnext.*");
5300 }
5301
5302 #[test]
5303 fn inscriptions_page_with_no_next() {
5304 let server = TestServer::builder()
5305 .chain(Chain::Regtest)
5306 .index_sats()
5307 .build();
5308
5309 for i in 0..101 {
5310 server.mine_blocks(1);
5311 server.core.broadcast_tx(TransactionTemplate {
5312 inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
5313 ..default()
5314 });
5315 }
5316
5317 server.mine_blocks(1);
5318
5319 server.assert_response_regex(
5320 "/inscriptions/1",
5321 StatusCode::OK,
5322 ".*<a class=prev href=/inscriptions/0>prev</a>\nnext.*",
5323 );
5324 }
5325
5326 #[test]
5327 fn inscriptions_page_with_no_prev() {
5328 let server = TestServer::builder()
5329 .chain(Chain::Regtest)
5330 .index_sats()
5331 .build();
5332
5333 for i in 0..101 {
5334 server.mine_blocks(1);
5335 server.core.broadcast_tx(TransactionTemplate {
5336 inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
5337 ..default()
5338 });
5339 }
5340
5341 server.mine_blocks(1);
5342
5343 server.assert_response_regex(
5344 "/inscriptions/0",
5345 StatusCode::OK,
5346 ".*prev\n<a class=next href=/inscriptions/1>next</a>.*",
5347 );
5348 }
5349
5350 #[test]
5351 fn collections_page_prev_and_next() {
5352 let server = TestServer::builder()
5353 .chain(Chain::Regtest)
5354 .index_sats()
5355 .build();
5356
5357 let mut parent_ids = Vec::new();
5358
5359 for i in 0..101 {
5360 server.mine_blocks(1);
5361
5362 parent_ids.push(InscriptionId {
5363 txid: server.core.broadcast_tx(TransactionTemplate {
5364 inputs: &[(i + 1, 0, 0, inscription("image/png", "hello").to_witness())],
5365 ..default()
5366 }),
5367 index: 0,
5368 });
5369 }
5370
5371 for (i, parent_id) in parent_ids.iter().enumerate().take(101) {
5372 server.mine_blocks(1);
5373
5374 server.core.broadcast_tx(TransactionTemplate {
5375 inputs: &[
5376 (i + 2, 1, 0, Default::default()),
5377 (
5378 i + 102,
5379 0,
5380 0,
5381 Inscription {
5382 content_type: Some("text/plain".into()),
5383 body: Some("hello".into()),
5384 parents: vec![parent_id.value()],
5385 ..default()
5386 }
5387 .to_witness(),
5388 ),
5389 ],
5390 outputs: 2,
5391 output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
5392 ..default()
5393 });
5394 }
5395
5396 server.mine_blocks(1);
5397
5398 server.assert_response_regex(
5399 "/collections",
5400 StatusCode::OK,
5401 r".*
5402<h1>Collections</h1>
5403<div class=thumbnails>
5404 <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
5405 (<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>\s*){99}
5406</div>
5407<div class=center>
5408prev
5409<a class=next href=/collections/1>next</a>
5410</div>.*"
5411 .to_string()
5412 .unindent(),
5413 );
5414
5415 server.assert_response_regex(
5416 "/collections/1",
5417 StatusCode::OK,
5418 ".*
5419<h1>Collections</h1>
5420<div class=thumbnails>
5421 <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
5422</div>
5423<div class=center>
5424<a class=prev href=/collections/0>prev</a>
5425next
5426</div>.*"
5427 .unindent(),
5428 );
5429 }
5430
5431 #[test]
5432 fn collections_page_ordered_by_most_recent_child() {
5433 let server = TestServer::builder()
5434 .chain(Chain::Regtest)
5435 .index_sats()
5436 .build();
5437
5438 server.mine_blocks(1);
5439 let parent_a = InscriptionId {
5440 txid: server.core.broadcast_tx(TransactionTemplate {
5441 inputs: &[(1, 0, 0, inscription("image/png", "parent a").to_witness())],
5442 ..default()
5443 }),
5444 index: 0,
5445 };
5446
5447 server.mine_blocks(1);
5448 let parent_b = InscriptionId {
5449 txid: server.core.broadcast_tx(TransactionTemplate {
5450 inputs: &[(2, 0, 0, inscription("image/png", "parent b").to_witness())],
5451 ..default()
5452 }),
5453 index: 0,
5454 };
5455
5456 server.mine_blocks(1);
5457 let parent_c = InscriptionId {
5458 txid: server.core.broadcast_tx(TransactionTemplate {
5459 inputs: &[(3, 0, 0, inscription("image/png", "parent c").to_witness())],
5460 ..default()
5461 }),
5462 index: 0,
5463 };
5464
5465 server.mine_blocks(1);
5466 server.mine_blocks(1);
5467
5468 server.core.broadcast_tx(TransactionTemplate {
5469 inputs: &[
5470 (2, 1, 0, Default::default()),
5471 (
5472 4,
5473 0,
5474 0,
5475 Inscription {
5476 content_type: Some("text/plain".into()),
5477 body: Some("child a1".into()),
5478 parents: vec![parent_a.value()],
5479 ..default()
5480 }
5481 .to_witness(),
5482 ),
5483 ],
5484 outputs: 2,
5485 output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
5486 ..default()
5487 });
5488
5489 server.mine_blocks(1);
5490
5491 server.core.broadcast_tx(TransactionTemplate {
5492 inputs: &[
5493 (3, 1, 0, Default::default()),
5494 (
5495 5,
5496 0,
5497 0,
5498 Inscription {
5499 content_type: Some("text/plain".into()),
5500 body: Some("child b1".into()),
5501 parents: vec![parent_b.value()],
5502 ..default()
5503 }
5504 .to_witness(),
5505 ),
5506 ],
5507 outputs: 2,
5508 output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
5509 ..default()
5510 });
5511
5512 server.mine_blocks(1);
5513
5514 server.core.broadcast_tx(TransactionTemplate {
5515 inputs: &[
5516 (4, 1, 0, Default::default()),
5517 (
5518 6,
5519 0,
5520 0,
5521 Inscription {
5522 content_type: Some("text/plain".into()),
5523 body: Some("child c1".into()),
5524 parents: vec![parent_c.value()],
5525 ..default()
5526 }
5527 .to_witness(),
5528 ),
5529 ],
5530 outputs: 2,
5531 output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
5532 ..default()
5533 });
5534
5535 server.mine_blocks(1);
5536
5537 server.assert_response_regex(
5538 "/collections",
5539 StatusCode::OK,
5540 format!(
5541 ".*<h1>Collections</h1>\n<div class=thumbnails>\n <a href=/inscription/{parent_c}>.*</a>\n <a href=/inscription/{parent_b}>.*</a>\n <a href=/inscription/{parent_a}>.*</a>\n</div>.*"
5542 ),
5543 );
5544
5545 server.core.broadcast_tx(TransactionTemplate {
5546 inputs: &[
5547 (6, 1, 0, Default::default()),
5548 (
5549 7,
5550 0,
5551 0,
5552 Inscription {
5553 content_type: Some("text/plain".into()),
5554 body: Some("child a2".into()),
5555 parents: vec![parent_a.value()],
5556 ..default()
5557 }
5558 .to_witness(),
5559 ),
5560 ],
5561 outputs: 2,
5562 output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
5563 ..default()
5564 });
5565
5566 server.mine_blocks(1);
5567
5568 server.assert_response_regex(
5569 "/collections",
5570 StatusCode::OK,
5571 format!(
5572 ".*<h1>Collections</h1>\n<div class=thumbnails>\n <a href=/inscription/{parent_a}>.*</a>\n <a href=/inscription/{parent_c}>.*</a>\n <a href=/inscription/{parent_b}>.*</a>\n</div>.*"
5573 ),
5574 );
5575 }
5576
5577 #[test]
5578 fn collections_page_shows_both_parents_of_multi_parent_child() {
5579 let server = TestServer::builder()
5580 .chain(Chain::Regtest)
5581 .index_sats()
5582 .build();
5583
5584 server.mine_blocks(1);
5585 let parent_a = InscriptionId {
5586 txid: server.core.broadcast_tx(TransactionTemplate {
5587 inputs: &[(1, 0, 0, inscription("image/png", "parent a").to_witness())],
5588 ..default()
5589 }),
5590 index: 0,
5591 };
5592
5593 server.mine_blocks(1);
5594 let parent_b = InscriptionId {
5595 txid: server.core.broadcast_tx(TransactionTemplate {
5596 inputs: &[(2, 0, 0, inscription("image/png", "parent b").to_witness())],
5597 ..default()
5598 }),
5599 index: 0,
5600 };
5601
5602 server.mine_blocks(1);
5603
5604 server.core.broadcast_tx(TransactionTemplate {
5605 inputs: &[
5606 (2, 1, 0, Default::default()),
5607 (3, 1, 0, Default::default()),
5608 (
5609 3,
5610 0,
5611 0,
5612 Inscription {
5613 content_type: Some("text/plain".into()),
5614 body: Some("child".into()),
5615 parents: vec![parent_a.value(), parent_b.value()],
5616 ..default()
5617 }
5618 .to_witness(),
5619 ),
5620 ],
5621 outputs: 3,
5622 output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE, 50 * COIN_VALUE],
5623 ..default()
5624 });
5625
5626 server.mine_blocks(1);
5627
5628 server.assert_response_regex(
5629 "/collections",
5630 StatusCode::OK,
5631 format!(
5632 ".*<h1>Collections</h1>\n<div class=thumbnails>\n <a href=/inscription/{parent_a}>.*</a>\n <a href=/inscription/{parent_b}>.*</a>\n</div>.*"
5633 ),
5634 );
5635 }
5636
5637 #[test]
5638 fn galleries_page_prev_and_next() {
5639 let server = TestServer::builder()
5640 .chain(Chain::Regtest)
5641 .index_sats()
5642 .build();
5643
5644 let mut gallery_item_ids = Vec::new();
5645
5646 for i in 0..15 {
5647 server.mine_blocks(1);
5648
5649 gallery_item_ids.push(InscriptionId {
5650 txid: server.core.broadcast_tx(TransactionTemplate {
5651 inputs: &[(
5652 i + 1,
5653 0,
5654 0,
5655 inscription("image/png", "gallery item").to_witness(),
5656 )],
5657 ..default()
5658 }),
5659 index: 0,
5660 });
5661 }
5662
5663 for i in 0..101 {
5664 server.mine_blocks(1);
5665
5666 let gallery_items = gallery_item_ids
5667 .iter()
5668 .cycle()
5669 .skip((i * 3) % gallery_item_ids.len())
5670 .take(3)
5671 .map(|&id| properties::Item {
5672 id: Some(id),
5673 attributes: Attributes::default(),
5674 index: None,
5675 })
5676 .collect::<Vec<_>>();
5677
5678 let properties = Properties {
5679 attributes: Attributes::default(),
5680 gallery: gallery_items,
5681 txids: Vec::new(),
5682 };
5683
5684 server.core.broadcast_tx(TransactionTemplate {
5685 inputs: &[(
5686 i + 16,
5687 0,
5688 0,
5689 Inscription {
5690 content_type: Some("image/png".into()),
5691 body: Some("gallery".into()),
5692 properties: properties.to_inline_cbor(),
5693 ..default()
5694 }
5695 .to_witness(),
5696 )],
5697 ..default()
5698 });
5699 }
5700
5701 server.mine_blocks(1);
5702
5703 server.assert_response_regex(
5704 "/galleries",
5705 StatusCode::OK,
5706 r".*
5707<h1>Galleries</h1>
5708<div class=thumbnails>
5709 <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
5710 (<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>\s*){99}
5711</div>
5712<div class=center>
5713prev
5714<a class=next href=/galleries/1>next</a>
5715</div>.*"
5716 .to_string()
5717 .unindent(),
5718 );
5719
5720 server.assert_response_regex(
5721 "/galleries/1",
5722 StatusCode::OK,
5723 ".*
5724<h1>Galleries</h1>
5725<div class=thumbnails>
5726 <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
5727</div>
5728<div class=center>
5729<a class=prev href=/galleries/0>prev</a>
5730next
5731</div>.*"
5732 .unindent(),
5733 );
5734
5735 server.assert_response_regex(
5736 "/galleries",
5737 StatusCode::OK,
5738 r".*<a href=/galleries title=galleries><img class=icon src=/static/box-archive\.svg></a>.*",
5739 );
5740 }
5741
5742 #[test]
5743 fn gallery_page_prev_and_next() {
5744 let server = TestServer::builder().chain(Chain::Regtest).build();
5745
5746 let mut gallery_item_ids = Vec::new();
5747
5748 for i in 0..101 {
5749 server.mine_blocks(1);
5750
5751 gallery_item_ids.push(InscriptionId {
5752 txid: server.core.broadcast_tx(TransactionTemplate {
5753 inputs: &[(
5754 i + 1,
5755 0,
5756 0,
5757 inscription("text/plain", "gallery item").to_witness(),
5758 )],
5759 ..default()
5760 }),
5761 index: 0,
5762 });
5763 }
5764
5765 let gallery_items = gallery_item_ids
5766 .iter()
5767 .map(|&id| properties::Item {
5768 id: Some(id),
5769 attributes: Attributes::default(),
5770 index: None,
5771 })
5772 .collect::<Vec<_>>();
5773
5774 let properties = Properties {
5775 attributes: Attributes::default(),
5776 gallery: gallery_items,
5777 txids: Vec::new(),
5778 };
5779
5780 server.mine_blocks(1);
5781
5782 let gallery_id = InscriptionId {
5783 txid: server.core.broadcast_tx(TransactionTemplate {
5784 inputs: &[(
5785 102,
5786 0,
5787 0,
5788 Inscription {
5789 content_type: Some("text/plain".into()),
5790 body: Some("gallery".into()),
5791 properties: properties.to_inline_cbor(),
5792 ..default()
5793 }
5794 .to_witness(),
5795 )],
5796 ..default()
5797 }),
5798 index: 0,
5799 };
5800
5801 server.mine_blocks(1);
5802
5803 let response = server.get(format!("/gallery/{gallery_id}"));
5804 assert_eq!(response.status(), StatusCode::OK);
5805 let body = response.text().unwrap();
5806 assert!(body.contains(&format!(
5807 "<h1><a href=/inscription/{gallery_id}>Inscription"
5808 )));
5809 assert!(body.contains(&format!("href=/gallery/{gallery_id}/0")));
5810 assert!(body.contains(&format!(
5811 "<a class=next href=/gallery/{gallery_id}/page/1>next</a>"
5812 )));
5813
5814 let response = server.get(format!("/gallery/{gallery_id}/page/1"));
5815 assert_eq!(response.status(), StatusCode::OK);
5816 let body = response.text().unwrap();
5817 assert!(body.contains(&format!(
5818 "<h1><a href=/inscription/{gallery_id}>Inscription"
5819 )));
5820 assert!(body.contains(&format!("href=/gallery/{gallery_id}/100")));
5821 assert!(body.contains(&format!(
5822 "<a class=prev href=/gallery/{gallery_id}/page/0>prev</a>"
5823 )));
5824 }
5825
5826 #[test]
5827 fn galleries_json() {
5828 let server = TestServer::builder()
5829 .chain(Chain::Regtest)
5830 .index_sats()
5831 .build();
5832
5833 server.mine_blocks(1);
5834
5835 let item_id = InscriptionId {
5836 txid: server.core.broadcast_tx(TransactionTemplate {
5837 inputs: &[(1, 0, 0, inscription("image/png", "foo").to_witness())],
5838 ..default()
5839 }),
5840 index: 0,
5841 };
5842
5843 server.mine_blocks(1);
5844
5845 let gallery_id = InscriptionId {
5846 txid: server.core.broadcast_tx(TransactionTemplate {
5847 inputs: &[(
5848 2,
5849 0,
5850 0,
5851 Inscription {
5852 content_type: Some("image/png".into()),
5853 body: Some("bar".into()),
5854 properties: Properties {
5855 gallery: vec![Item {
5856 id: Some(item_id),
5857 ..default()
5858 }],
5859 ..default()
5860 }
5861 .to_inline_cbor(),
5862 ..default()
5863 }
5864 .to_witness(),
5865 )],
5866 ..default()
5867 }),
5868 index: 0,
5869 };
5870
5871 server.mine_blocks(1);
5872
5873 let json: api::Inscriptions = server.get_json("/galleries");
5874
5875 assert_eq!(json.ids, vec![gallery_id]);
5876 assert!(!json.more);
5877 assert_eq!(json.page_index, 0);
5878 }
5879
5880 #[test]
5881 fn galleries_json_pagination() {
5882 let server = TestServer::builder()
5883 .chain(Chain::Regtest)
5884 .index_sats()
5885 .build();
5886
5887 let mut item_ids = Vec::new();
5888
5889 for i in 0..3 {
5890 server.mine_blocks(1);
5891
5892 item_ids.push(InscriptionId {
5893 txid: server.core.broadcast_tx(TransactionTemplate {
5894 inputs: &[(i + 1, 0, 0, inscription("image/png", "foo").to_witness())],
5895 ..default()
5896 }),
5897 index: 0,
5898 });
5899 }
5900
5901 let mut gallery_ids = Vec::new();
5902
5903 for i in 0..101 {
5904 server.mine_blocks(1);
5905
5906 gallery_ids.push(InscriptionId {
5907 txid: server.core.broadcast_tx(TransactionTemplate {
5908 inputs: &[(
5909 i + 4,
5910 0,
5911 0,
5912 Inscription {
5913 content_type: Some("image/png".into()),
5914 body: Some("bar".into()),
5915 properties: Properties {
5916 gallery: vec![Item {
5917 id: Some(item_ids[i % item_ids.len()]),
5918 ..default()
5919 }],
5920 ..default()
5921 }
5922 .to_inline_cbor(),
5923 ..default()
5924 }
5925 .to_witness(),
5926 )],
5927 ..default()
5928 }),
5929 index: 0,
5930 });
5931 }
5932
5933 server.mine_blocks(1);
5934
5935 let json: api::Inscriptions = server.get_json("/galleries");
5936
5937 assert_eq!(json.ids.len(), 100);
5938 assert!(json.more);
5939 assert_eq!(json.page_index, 0);
5940
5941 let json: api::Inscriptions = server.get_json("/galleries/1");
5942
5943 assert_eq!(json.ids.len(), 1);
5944 assert!(!json.more);
5945 assert_eq!(json.page_index, 1);
5946 }
5947
5948 #[test]
5949 fn non_gallery_inscription_not_in_galleries() {
5950 let server = TestServer::builder()
5951 .chain(Chain::Regtest)
5952 .index_sats()
5953 .build();
5954
5955 server.mine_blocks(1);
5956
5957 server.core.broadcast_tx(TransactionTemplate {
5958 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
5959 ..default()
5960 });
5961
5962 server.mine_blocks(1);
5963
5964 let json: api::Inscriptions = server.get_json("/galleries");
5965
5966 assert!(json.ids.is_empty());
5967 assert!(!json.more);
5968 }
5969
5970 #[test]
5971 fn hidden_galleries_are_excluded() {
5972 let server = TestServer::builder()
5973 .chain(Chain::Regtest)
5974 .index_sats()
5975 .build();
5976
5977 server.mine_blocks(1);
5978
5979 let item_id = InscriptionId {
5980 txid: server.core.broadcast_tx(TransactionTemplate {
5981 inputs: &[(1, 0, 0, inscription("image/png", "foo").to_witness())],
5982 ..default()
5983 }),
5984 index: 0,
5985 };
5986
5987 server.mine_blocks(1);
5988
5989 server.core.broadcast_tx(TransactionTemplate {
5990 inputs: &[(
5991 2,
5992 0,
5993 0,
5994 Inscription {
5995 content_type: Some("text/plain".into()),
5996 body: Some("bar".into()),
5997 properties: Properties {
5998 gallery: vec![Item {
5999 id: Some(item_id),
6000 ..default()
6001 }],
6002 ..default()
6003 }
6004 .to_inline_cbor(),
6005 ..default()
6006 }
6007 .to_witness(),
6008 )],
6009 ..default()
6010 });
6011
6012 server.mine_blocks(1);
6013
6014 let visible_gallery_id = InscriptionId {
6015 txid: server.core.broadcast_tx(TransactionTemplate {
6016 inputs: &[(
6017 3,
6018 0,
6019 0,
6020 Inscription {
6021 content_type: Some("image/png".into()),
6022 body: Some("baz".into()),
6023 properties: Properties {
6024 gallery: vec![Item {
6025 id: Some(item_id),
6026 ..default()
6027 }],
6028 ..default()
6029 }
6030 .to_inline_cbor(),
6031 ..default()
6032 }
6033 .to_witness(),
6034 )],
6035 ..default()
6036 }),
6037 index: 0,
6038 };
6039
6040 server.mine_blocks(1);
6041
6042 let json: api::Inscriptions = server.get_json("/galleries");
6043
6044 assert_eq!(json.ids, vec![visible_gallery_id]);
6045 }
6046
6047 #[test]
6048 fn hidden_collections_are_excluded() {
6049 let server = TestServer::builder()
6050 .chain(Chain::Regtest)
6051 .index_sats()
6052 .build();
6053
6054 server.mine_blocks(1);
6055
6056 let hidden_parent = InscriptionId {
6057 txid: server.core.broadcast_tx(TransactionTemplate {
6058 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
6059 ..default()
6060 }),
6061 index: 0,
6062 };
6063
6064 server.mine_blocks(1);
6065
6066 let visible_parent = InscriptionId {
6067 txid: server.core.broadcast_tx(TransactionTemplate {
6068 inputs: &[(2, 0, 0, inscription("image/png", "bar").to_witness())],
6069 ..default()
6070 }),
6071 index: 0,
6072 };
6073
6074 server.mine_blocks(1);
6075
6076 server.core.broadcast_tx(TransactionTemplate {
6077 inputs: &[
6078 (2, 1, 0, Default::default()),
6079 (
6080 3,
6081 0,
6082 0,
6083 Inscription {
6084 content_type: Some("text/plain".into()),
6085 body: Some("baz".into()),
6086 parents: vec![hidden_parent.value()],
6087 ..default()
6088 }
6089 .to_witness(),
6090 ),
6091 ],
6092 outputs: 2,
6093 output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
6094 ..default()
6095 });
6096
6097 server.mine_blocks(1);
6098
6099 server.core.broadcast_tx(TransactionTemplate {
6100 inputs: &[
6101 (3, 1, 0, Default::default()),
6102 (
6103 4,
6104 0,
6105 0,
6106 Inscription {
6107 content_type: Some("text/plain".into()),
6108 body: Some("qux".into()),
6109 parents: vec![visible_parent.value()],
6110 ..default()
6111 }
6112 .to_witness(),
6113 ),
6114 ],
6115 outputs: 2,
6116 output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
6117 ..default()
6118 });
6119
6120 server.mine_blocks(1);
6121
6122 server.assert_response_regex(
6123 "/collections",
6124 StatusCode::OK,
6125 format!(
6126 ".*<h1>Collections</h1>\n<div class=thumbnails>\n <a href=/inscription/{visible_parent}>.*</a>\n</div>.*"
6127 ),
6128 );
6129 }
6130
6131 #[test]
6132 fn responses_are_gzipped() {
6133 let server = TestServer::new();
6134
6135 let mut headers = HeaderMap::new();
6136
6137 headers.insert(header::ACCEPT_ENCODING, "gzip".parse().unwrap());
6138
6139 let response = reqwest::blocking::Client::builder()
6140 .default_headers(headers)
6141 .build()
6142 .unwrap()
6143 .get(server.join_url("/"))
6144 .send()
6145 .unwrap();
6146
6147 assert_eq!(
6148 response.headers().get(header::CONTENT_ENCODING).unwrap(),
6149 "gzip"
6150 );
6151 }
6152
6153 #[test]
6154 fn responses_are_brotlied() {
6155 let server = TestServer::new();
6156
6157 let mut headers = HeaderMap::new();
6158
6159 headers.insert(header::ACCEPT_ENCODING, BROTLI.parse().unwrap());
6160
6161 let response = reqwest::blocking::Client::builder()
6162 .default_headers(headers)
6163 .brotli(false)
6164 .build()
6165 .unwrap()
6166 .get(server.join_url("/"))
6167 .send()
6168 .unwrap();
6169
6170 assert_eq!(
6171 response.headers().get(header::CONTENT_ENCODING).unwrap(),
6172 BROTLI
6173 );
6174 }
6175
6176 #[test]
6177 fn inscription_links_to_parent() {
6178 let server = TestServer::builder().chain(Chain::Regtest).build();
6179 server.mine_blocks(1);
6180
6181 let parent_txid = server.core.broadcast_tx(TransactionTemplate {
6182 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
6183 ..default()
6184 });
6185
6186 server.mine_blocks(1);
6187
6188 let parent_inscription_id = InscriptionId {
6189 txid: parent_txid,
6190 index: 0,
6191 };
6192
6193 let txid = server.core.broadcast_tx(TransactionTemplate {
6194 inputs: &[
6195 (
6196 2,
6197 0,
6198 0,
6199 Inscription {
6200 content_type: Some("text/plain".into()),
6201 body: Some("hello".into()),
6202 parents: vec![parent_inscription_id.value()],
6203 ..default()
6204 }
6205 .to_witness(),
6206 ),
6207 (2, 1, 0, Default::default()),
6208 ],
6209 ..default()
6210 });
6211
6212 server.mine_blocks(1);
6213
6214 let inscription_id = InscriptionId { txid, index: 0 };
6215
6216 server.assert_response_regex(
6217 format!("/inscription/{inscription_id}"),
6218 StatusCode::OK,
6219 format!(".*<title>Inscription 1</title>.*<dt>parents</dt>.*<div class=thumbnails>.**<a href=/inscription/{parent_inscription_id}><iframe .* src=/preview/{parent_inscription_id}></iframe></a>.*"),
6220 );
6221 server.assert_response_regex(
6222 format!("/inscription/{parent_inscription_id}"),
6223 StatusCode::OK,
6224 format!(".*<title>Inscription 0</title>.*<dt>children</dt>.*<a href=/inscription/{inscription_id}>.*</a>.*"),
6225 );
6226
6227 assert_eq!(
6228 server
6229 .get_json::<api::Inscription>(format!("/inscription/{inscription_id}"))
6230 .parents,
6231 vec![parent_inscription_id],
6232 );
6233
6234 assert_eq!(
6235 server
6236 .get_json::<api::Inscription>(format!("/inscription/{parent_inscription_id}"))
6237 .children,
6238 [inscription_id],
6239 );
6240 }
6241
6242 #[test]
6243 fn inscription_with_and_without_children_page() {
6244 let server = TestServer::builder().chain(Chain::Regtest).build();
6245 server.mine_blocks(1);
6246
6247 let parent_txid = server.core.broadcast_tx(TransactionTemplate {
6248 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
6249 ..default()
6250 });
6251
6252 server.mine_blocks(1);
6253
6254 let parent_inscription_id = InscriptionId {
6255 txid: parent_txid,
6256 index: 0,
6257 };
6258
6259 server.assert_response_regex(
6260 format!("/children/{parent_inscription_id}"),
6261 StatusCode::OK,
6262 ".*<h3>No children</h3>.*",
6263 );
6264
6265 let txid = server.core.broadcast_tx(TransactionTemplate {
6266 inputs: &[
6267 (
6268 2,
6269 0,
6270 0,
6271 Inscription {
6272 content_type: Some("text/plain".into()),
6273 body: Some("hello".into()),
6274 parents: vec![parent_inscription_id.value()],
6275 ..default()
6276 }
6277 .to_witness(),
6278 ),
6279 (2, 1, 0, Default::default()),
6280 ],
6281 ..default()
6282 });
6283
6284 server.mine_blocks(1);
6285
6286 let inscription_id = InscriptionId { txid, index: 0 };
6287
6288 server.assert_response_regex(
6289 format!("/children/{parent_inscription_id}"),
6290 StatusCode::OK,
6291 format!(".*<title>Inscription 0 Children</title>.*<h1><a href=/inscription/{parent_inscription_id}>Inscription 0</a> Children</h1>.*<div class=thumbnails>.*<a href=/inscription/{inscription_id}><iframe .* src=/preview/{inscription_id}></iframe></a>.*"),
6292 );
6293 }
6294
6295 #[test]
6296 fn inscription_with_and_without_gallery_page() {
6297 let server = TestServer::builder().chain(Chain::Regtest).build();
6298 server.mine_blocks(1);
6299
6300 let empty_gallery_txid = server.core.broadcast_tx(TransactionTemplate {
6301 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
6302 ..default()
6303 });
6304
6305 server.mine_blocks(1);
6306
6307 let empty_gallery_id = InscriptionId {
6308 txid: empty_gallery_txid,
6309 index: 0,
6310 };
6311
6312 server.assert_response_regex(
6313 format!("/gallery/{empty_gallery_id}"),
6314 StatusCode::OK,
6315 ".*<h3>No gallery items</h3>.*",
6316 );
6317
6318 let item_txid = server.core.broadcast_tx(TransactionTemplate {
6319 inputs: &[(2, 0, 0, inscription("text/plain", "item").to_witness())],
6320 ..default()
6321 });
6322
6323 server.mine_blocks(1);
6324
6325 let item_id = InscriptionId {
6326 txid: item_txid,
6327 index: 0,
6328 };
6329
6330 let gallery_txid = server.core.broadcast_tx(TransactionTemplate {
6331 inputs: &[(
6332 3,
6333 0,
6334 0,
6335 Inscription {
6336 content_type: Some("text/plain".into()),
6337 body: Some("gallery".into()),
6338 properties: Properties {
6339 gallery: vec![Item {
6340 id: Some(item_id),
6341 ..default()
6342 }],
6343 ..default()
6344 }
6345 .to_inline_cbor(),
6346 ..default()
6347 }
6348 .to_witness(),
6349 )],
6350 ..default()
6351 });
6352
6353 server.mine_blocks(1);
6354
6355 let gallery_id = InscriptionId {
6356 txid: gallery_txid,
6357 index: 0,
6358 };
6359
6360 server.assert_response_regex(
6361 format!("/gallery/{gallery_id}"),
6362 StatusCode::OK,
6363 format!(
6364 ".*<title>Inscription \\d+ Gallery</title>.*<h1><a href=/inscription/{gallery_id}>Inscription \\d+</a> Gallery</h1>.*<div class=thumbnails>.*<a href=/gallery/{gallery_id}/0><iframe .* src=/preview/{item_id}></iframe></a>.*",
6365 ),
6366 );
6367 }
6368
6369 #[test]
6370 fn inscriptions_page_shows_max_four_children() {
6371 let server = TestServer::builder().chain(Chain::Regtest).build();
6372 server.mine_blocks(1);
6373
6374 let parent_txid = server.core.broadcast_tx(TransactionTemplate {
6375 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
6376 ..default()
6377 });
6378
6379 server.mine_blocks(6);
6380
6381 let parent_inscription_id = InscriptionId {
6382 txid: parent_txid,
6383 index: 0,
6384 };
6385
6386 let _txid = server.core.broadcast_tx(TransactionTemplate {
6387 inputs: &[
6388 (
6389 2,
6390 0,
6391 0,
6392 Inscription {
6393 content_type: Some("text/plain".into()),
6394 body: Some("hello".into()),
6395 parents: vec![parent_inscription_id.value()],
6396 ..default()
6397 }
6398 .to_witness(),
6399 ),
6400 (
6401 3,
6402 0,
6403 0,
6404 Inscription {
6405 content_type: Some("text/plain".into()),
6406 body: Some("hello".into()),
6407 parents: vec![parent_inscription_id.value()],
6408 ..default()
6409 }
6410 .to_witness(),
6411 ),
6412 (
6413 4,
6414 0,
6415 0,
6416 Inscription {
6417 content_type: Some("text/plain".into()),
6418 body: Some("hello".into()),
6419 parents: vec![parent_inscription_id.value()],
6420 ..default()
6421 }
6422 .to_witness(),
6423 ),
6424 (
6425 5,
6426 0,
6427 0,
6428 Inscription {
6429 content_type: Some("text/plain".into()),
6430 body: Some("hello".into()),
6431 parents: vec![parent_inscription_id.value()],
6432 ..default()
6433 }
6434 .to_witness(),
6435 ),
6436 (
6437 6,
6438 0,
6439 0,
6440 Inscription {
6441 content_type: Some("text/plain".into()),
6442 body: Some("hello".into()),
6443 parents: vec![parent_inscription_id.value()],
6444 ..default()
6445 }
6446 .to_witness(),
6447 ),
6448 (2, 1, 0, Default::default()),
6449 ],
6450 ..default()
6451 });
6452
6453 server.mine_blocks(1);
6454
6455 server.assert_response_regex(
6456 format!("/inscription/{parent_inscription_id}"),
6457 StatusCode::OK,
6458 format!(
6459 ".*<title>Inscription 0</title>.*
6460.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
6461.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
6462.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
6463.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
6464 <div class=center>
6465 <a href=/children/{parent_inscription_id}>all \\(5\\)</a>
6466 </div>.*"
6467 ),
6468 );
6469 }
6470
6471 #[test]
6472 fn inscriptions_page_shows_max_four_gallery_items() {
6473 let server = TestServer::builder().chain(Chain::Regtest).build();
6474
6475 let mut item_ids = Vec::new();
6476
6477 for i in 0..5 {
6478 server.mine_blocks(1);
6479
6480 item_ids.push(InscriptionId {
6481 txid: server.core.broadcast_tx(TransactionTemplate {
6482 inputs: &[(
6483 i + 1,
6484 0,
6485 0,
6486 inscription("text/plain", "gallery item").to_witness(),
6487 )],
6488 ..default()
6489 }),
6490 index: 0,
6491 });
6492 }
6493
6494 let gallery_items = item_ids
6495 .into_iter()
6496 .map(|id| Item {
6497 id: Some(id),
6498 ..default()
6499 })
6500 .collect::<Vec<_>>();
6501
6502 let properties = Properties {
6503 attributes: Attributes::default(),
6504 gallery: gallery_items,
6505 txids: Vec::new(),
6506 };
6507
6508 server.mine_blocks(1);
6509
6510 let gallery_txid = server.core.broadcast_tx(TransactionTemplate {
6511 inputs: &[(
6512 6,
6513 0,
6514 0,
6515 Inscription {
6516 content_type: Some("text/plain".into()),
6517 body: Some("gallery".into()),
6518 properties: properties.to_inline_cbor(),
6519 ..default()
6520 }
6521 .to_witness(),
6522 )],
6523 ..default()
6524 });
6525
6526 server.mine_blocks(1);
6527
6528 let gallery_id = InscriptionId {
6529 txid: gallery_txid,
6530 index: 0,
6531 };
6532
6533 server.assert_response_regex(
6534 format!("/inscription/{gallery_id}"),
6535 StatusCode::OK,
6536 format!(
6537 ".*<title>Inscription \\d+</title>.*
6538.*<dt>gallery</dt>.*
6539.*<a href=/gallery/{gallery_id}/.*><iframe .* src=/preview/.*></iframe></a>.*
6540.*<a href=/gallery/{gallery_id}/.*><iframe .* src=/preview/.*></iframe></a>.*
6541.*<a href=/gallery/{gallery_id}/.*><iframe .* src=/preview/.*></iframe></a>.*
6542.*<a href=/gallery/{gallery_id}/.*><iframe .* src=/preview/.*></iframe></a>.*
6543 <div class=center>
6544 <a href=/gallery/{gallery_id}>all \\(5\\)</a>
6545 </div>.*"
6546 ),
6547 );
6548 }
6549
6550 #[test]
6551 fn inscription_child() {
6552 let server = TestServer::builder().chain(Chain::Regtest).build();
6553 server.mine_blocks(1);
6554
6555 let parent_txid = server.core.broadcast_tx(TransactionTemplate {
6556 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
6557 ..default()
6558 });
6559
6560 server.mine_blocks(2);
6561
6562 let parent_inscription_id = InscriptionId {
6563 txid: parent_txid,
6564 index: 0,
6565 };
6566
6567 let child_txid = server.core.broadcast_tx(TransactionTemplate {
6568 inputs: &[
6569 (
6570 2,
6571 0,
6572 0,
6573 Inscription {
6574 content_type: Some("text/plain".into()),
6575 body: Some("hello".into()),
6576 parents: vec![parent_inscription_id.value()],
6577 ..default()
6578 }
6579 .to_witness(),
6580 ),
6581 (
6582 3,
6583 0,
6584 0,
6585 Inscription {
6586 content_type: Some("text/plain".into()),
6587 body: Some("hello".into()),
6588 parents: vec![parent_inscription_id.value()],
6589 ..default()
6590 }
6591 .to_witness(),
6592 ),
6593 (2, 1, 0, Default::default()),
6594 ],
6595 ..default()
6596 });
6597
6598 server.mine_blocks(1);
6599
6600 let child0 = InscriptionId {
6601 txid: child_txid,
6602 index: 0,
6603 };
6604
6605 server.assert_response_regex(
6606 format!("/inscription/{parent_inscription_id}/0"),
6607 StatusCode::OK,
6608 format!(
6609 ".*<title>Inscription 1</title>.*
6610.*<dt>id</dt>
6611.*<dd class=collapse>{child0}</dd>.*"
6612 ),
6613 );
6614
6615 let child1 = InscriptionId {
6616 txid: child_txid,
6617 index: 1,
6618 };
6619
6620 server.assert_response_regex(
6621 format!("/inscription/{parent_inscription_id}/1"),
6622 StatusCode::OK,
6623 format!(
6624 ".*<title>Inscription -1</title>.*
6625.*<dt>id</dt>
6626.*<dd class=collapse>{child1}</dd>.*"
6627 ),
6628 );
6629 }
6630
6631 #[test]
6632 fn inscription_with_parent_page() {
6633 let server = TestServer::builder().chain(Chain::Regtest).build();
6634 server.mine_blocks(2);
6635
6636 let parent_a_txid = server.core.broadcast_tx(TransactionTemplate {
6637 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
6638 ..default()
6639 });
6640
6641 let parent_b_txid = server.core.broadcast_tx(TransactionTemplate {
6642 inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())],
6643 ..default()
6644 });
6645
6646 server.mine_blocks(1);
6647
6648 let parent_a_inscription_id = InscriptionId {
6649 txid: parent_a_txid,
6650 index: 0,
6651 };
6652
6653 let parent_b_inscription_id = InscriptionId {
6654 txid: parent_b_txid,
6655 index: 0,
6656 };
6657
6658 let txid = server.core.broadcast_tx(TransactionTemplate {
6659 inputs: &[
6660 (
6661 3,
6662 0,
6663 0,
6664 Inscription {
6665 content_type: Some("text/plain".into()),
6666 body: Some("hello".into()),
6667 parents: vec![
6668 parent_a_inscription_id.value(),
6669 parent_b_inscription_id.value(),
6670 ],
6671 ..default()
6672 }
6673 .to_witness(),
6674 ),
6675 (3, 1, 0, Default::default()),
6676 (3, 2, 0, Default::default()),
6677 ],
6678 ..default()
6679 });
6680
6681 server.mine_blocks(1);
6682
6683 let inscription_id = InscriptionId { txid, index: 0 };
6684
6685 server.assert_response_regex(
6686 format!("/parents/{inscription_id}"),
6687 StatusCode::OK,
6688 format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>.*<a href=/inscription/{parent_a_inscription_id}><iframe .* src=/preview/{parent_b_inscription_id}></iframe></a>.*"),
6689 );
6690 }
6691
6692 #[test]
6693 fn inscription_parent_page_pagination() {
6694 let server = TestServer::builder().chain(Chain::Regtest).build();
6695
6696 server.mine_blocks(1);
6697
6698 let mut parent_ids = Vec::new();
6699 let mut inputs = Vec::new();
6700 for i in 0..101 {
6701 parent_ids.push(
6702 InscriptionId {
6703 txid: server.core.broadcast_tx(TransactionTemplate {
6704 inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
6705 ..default()
6706 }),
6707 index: 0,
6708 }
6709 .value(),
6710 );
6711
6712 inputs.push((i + 2, 1, 0, Witness::default()));
6713
6714 server.mine_blocks(1);
6715 }
6716
6717 inputs.insert(
6718 0,
6719 (
6720 102,
6721 0,
6722 0,
6723 Inscription {
6724 content_type: Some("text/plain".into()),
6725 body: Some("hello".into()),
6726 parents: parent_ids,
6727 ..default()
6728 }
6729 .to_witness(),
6730 ),
6731 );
6732
6733 let txid = server.core.broadcast_tx(TransactionTemplate {
6734 inputs: &inputs,
6735 ..default()
6736 });
6737
6738 server.mine_blocks(1);
6739
6740 let inscription_id = InscriptionId { txid, index: 0 };
6741
6742 server.assert_response_regex(
6743 format!("/parents/{inscription_id}"),
6744 StatusCode::OK,
6745 format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){{100}}.*"),
6746 );
6747
6748 server.assert_response_regex(
6749 format!("/parents/{inscription_id}/1"),
6750 StatusCode::OK,
6751 format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){{1}}.*"),
6752 );
6753
6754 server.assert_response_regex(
6755 format!("/inscription/{inscription_id}"),
6756 StatusCode::OK,
6757 ".*<title>Inscription -1</title>.*<h1>Inscription -1</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){4}.*",
6758 );
6759 }
6760
6761 #[test]
6762 fn inscription_number_endpoint() {
6763 let server = TestServer::builder().chain(Chain::Regtest).build();
6764 server.mine_blocks(2);
6765
6766 let txid = server.core.broadcast_tx(TransactionTemplate {
6767 inputs: &[
6768 (1, 0, 0, inscription("text/plain", "hello").to_witness()),
6769 (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
6770 ],
6771 outputs: 2,
6772 ..default()
6773 });
6774
6775 let inscription_id = InscriptionId { txid, index: 0 };
6776 let cursed_inscription_id = InscriptionId { txid, index: 1 };
6777
6778 server.mine_blocks(1);
6779
6780 server.assert_response_regex(
6781 format!("/inscription/{inscription_id}"),
6782 StatusCode::OK,
6783 format!(
6784 ".*<h1>Inscription 0</h1>.*
6785<dl>
6786 <dt>id</dt>
6787 <dd class=collapse>{inscription_id}</dd>.*"
6788 ),
6789 );
6790 server.assert_response_regex(
6791 "/inscription/0",
6792 StatusCode::OK,
6793 format!(
6794 ".*<h1>Inscription 0</h1>.*
6795<dl>
6796 <dt>id</dt>
6797 <dd class=collapse>{inscription_id}</dd>.*"
6798 ),
6799 );
6800
6801 server.assert_response_regex(
6802 "/inscription/-1",
6803 StatusCode::OK,
6804 format!(
6805 ".*<h1>Inscription -1</h1>.*
6806<dl>
6807 <dt>id</dt>
6808 <dd class=collapse>{cursed_inscription_id}</dd>.*"
6809 ),
6810 )
6811 }
6812
6813 #[test]
6814 fn charm_cursed() {
6815 let server = TestServer::builder().chain(Chain::Regtest).build();
6816
6817 server.mine_blocks(2);
6818
6819 let txid = server.core.broadcast_tx(TransactionTemplate {
6820 inputs: &[
6821 (1, 0, 0, Witness::default()),
6822 (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
6823 ],
6824 outputs: 2,
6825 ..default()
6826 });
6827
6828 let id = InscriptionId { txid, index: 0 };
6829
6830 server.mine_blocks(1);
6831
6832 server.assert_response_regex(
6833 format!("/inscription/{id}"),
6834 StatusCode::OK,
6835 format!(
6836 ".*<h1>Inscription -1</h1>.*
6837<dl>
6838 <dt>id</dt>
6839 <dd class=collapse>{id}</dd>
6840 <dt>charms</dt>
6841 <dd>
6842 <span title=cursed>👹</span>
6843 </dd>
6844 .*
6845</dl>
6846.*
6847"
6848 ),
6849 );
6850 }
6851
6852 #[test]
6853 fn charm_vindicated() {
6854 let server = TestServer::builder().chain(Chain::Regtest).build();
6855
6856 server.mine_blocks(110);
6857
6858 let txid = server.core.broadcast_tx(TransactionTemplate {
6859 inputs: &[
6860 (1, 0, 0, Witness::default()),
6861 (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
6862 ],
6863 outputs: 2,
6864 ..default()
6865 });
6866
6867 let id = InscriptionId { txid, index: 0 };
6868
6869 server.mine_blocks(1);
6870
6871 server.assert_response_regex(
6872 format!("/inscription/{id}"),
6873 StatusCode::OK,
6874 format!(
6875 ".*<h1>Inscription 0</h1>.*
6876<dl>
6877 <dt>id</dt>
6878 <dd class=collapse>{id}</dd>
6879 .*
6880 <dt>value</dt>
6881 .*
6882</dl>
6883.*
6884"
6885 ),
6886 );
6887 }
6888
6889 #[test]
6890 fn charm_coin() {
6891 let server = TestServer::builder()
6892 .chain(Chain::Regtest)
6893 .index_sats()
6894 .build();
6895
6896 server.mine_blocks(2);
6897
6898 let txid = server.core.broadcast_tx(TransactionTemplate {
6899 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
6900 ..default()
6901 });
6902
6903 let id = InscriptionId { txid, index: 0 };
6904
6905 server.mine_blocks(1);
6906
6907 server.assert_response_regex(
6908 format!("/inscription/{id}"),
6909 StatusCode::OK,
6910 format!(
6911 ".*<h1>Inscription 0</h1>.*
6912<dl>
6913 <dt>id</dt>
6914 <dd class=collapse>{id}</dd>
6915 <dt>charms</dt>
6916 <dd>.*<span title=coin>🪙</span>.*</dd>
6917 .*
6918</dl>
6919.*
6920"
6921 ),
6922 );
6923 }
6924
6925 #[test]
6926 fn charm_uncommon() {
6927 let server = TestServer::builder()
6928 .chain(Chain::Regtest)
6929 .index_sats()
6930 .build();
6931
6932 server.mine_blocks(2);
6933
6934 let txid = server.core.broadcast_tx(TransactionTemplate {
6935 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
6936 ..default()
6937 });
6938
6939 let id = InscriptionId { txid, index: 0 };
6940
6941 server.mine_blocks(1);
6942
6943 server.assert_response_regex(
6944 format!("/inscription/{id}"),
6945 StatusCode::OK,
6946 format!(
6947 ".*<h1>Inscription 0</h1>.*
6948<dl>
6949 <dt>id</dt>
6950 <dd class=collapse>{id}</dd>
6951 <dt>charms</dt>
6952 <dd>.*<span title=uncommon>🌱</span>.*</dd>
6953 .*
6954</dl>
6955.*
6956"
6957 ),
6958 );
6959 }
6960
6961 #[test]
6962 fn charm_nineball() {
6963 let server = TestServer::builder()
6964 .chain(Chain::Regtest)
6965 .index_sats()
6966 .build();
6967
6968 server.mine_blocks(9);
6969
6970 let txid = server.core.broadcast_tx(TransactionTemplate {
6971 inputs: &[(9, 0, 0, inscription("text/plain", "foo").to_witness())],
6972 ..default()
6973 });
6974
6975 let id = InscriptionId { txid, index: 0 };
6976
6977 server.mine_blocks(1);
6978
6979 server.assert_response_regex(
6980 format!("/inscription/{id}"),
6981 StatusCode::OK,
6982 format!(
6983 ".*<h1>Inscription 0</h1>.*
6984<dl>
6985 <dt>id</dt>
6986 <dd class=collapse>{id}</dd>
6987 <dt>charms</dt>
6988 <dd>.*<span title=nineball>9️⃣</span>.*</dd>
6989 .*
6990</dl>
6991.*
6992"
6993 ),
6994 );
6995 }
6996
6997 #[test]
6998 fn charm_reinscription() {
6999 let server = TestServer::builder().chain(Chain::Regtest).build();
7000
7001 server.mine_blocks(1);
7002
7003 server.core.broadcast_tx(TransactionTemplate {
7004 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
7005 ..default()
7006 });
7007
7008 server.mine_blocks(1);
7009
7010 let txid = server.core.broadcast_tx(TransactionTemplate {
7011 inputs: &[(2, 1, 0, inscription("text/plain", "bar").to_witness())],
7012 ..default()
7013 });
7014
7015 server.mine_blocks(1);
7016
7017 let id = InscriptionId { txid, index: 0 };
7018
7019 server.assert_response_regex(
7020 format!("/inscription/{id}"),
7021 StatusCode::OK,
7022 format!(
7023 ".*<h1>Inscription -1</h1>.*
7024<dl>
7025 <dt>id</dt>
7026 <dd class=collapse>{id}</dd>
7027 <dt>charms</dt>
7028 <dd>
7029 <span title=reinscription>♻️</span>
7030 <span title=cursed>👹</span>
7031 </dd>
7032 .*
7033</dl>
7034.*
7035"
7036 ),
7037 );
7038 }
7039
7040 #[test]
7041 fn charm_reinscription_in_same_tx_input() {
7042 let server = TestServer::builder().chain(Chain::Regtest).build();
7043
7044 server.mine_blocks(1);
7045
7046 let script = script::Builder::new()
7047 .push_opcode(opcodes::OP_FALSE)
7048 .push_opcode(opcodes::all::OP_IF)
7049 .push_slice(b"ord")
7050 .push_slice([1])
7051 .push_slice(b"text/plain;charset=utf-8")
7052 .push_slice([])
7053 .push_slice(b"foo")
7054 .push_opcode(opcodes::all::OP_ENDIF)
7055 .push_opcode(opcodes::OP_FALSE)
7056 .push_opcode(opcodes::all::OP_IF)
7057 .push_slice(b"ord")
7058 .push_slice([1])
7059 .push_slice(b"text/plain;charset=utf-8")
7060 .push_slice([])
7061 .push_slice(b"bar")
7062 .push_opcode(opcodes::all::OP_ENDIF)
7063 .push_opcode(opcodes::OP_FALSE)
7064 .push_opcode(opcodes::all::OP_IF)
7065 .push_slice(b"ord")
7066 .push_slice([1])
7067 .push_slice(b"text/plain;charset=utf-8")
7068 .push_slice([])
7069 .push_slice(b"qix")
7070 .push_opcode(opcodes::all::OP_ENDIF)
7071 .into_script();
7072
7073 let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);
7074
7075 let txid = server.core.broadcast_tx(TransactionTemplate {
7076 inputs: &[(1, 0, 0, witness)],
7077 ..default()
7078 });
7079
7080 server.mine_blocks(1);
7081
7082 let id = InscriptionId { txid, index: 0 };
7083 server.assert_response_regex(
7084 format!("/inscription/{id}"),
7085 StatusCode::OK,
7086 format!(
7087 ".*<h1>Inscription 0</h1>.*
7088<dl>
7089 <dt>id</dt>
7090 <dd class=collapse>{id}</dd>
7091 .*
7092 <dt>value</dt>
7093 .*
7094</dl>
7095.*
7096"
7097 ),
7098 );
7099
7100 let id = InscriptionId { txid, index: 1 };
7101 server.assert_response_regex(
7102 format!("/inscription/{id}"),
7103 StatusCode::OK,
7104 ".*
7105 <span title=reinscription>♻️</span>
7106 <span title=cursed>👹</span>.*",
7107 );
7108
7109 let id = InscriptionId { txid, index: 2 };
7110 server.assert_response_regex(
7111 format!("/inscription/{id}"),
7112 StatusCode::OK,
7113 ".*
7114 <span title=reinscription>♻️</span>
7115 <span title=cursed>👹</span>.*",
7116 );
7117 }
7118
7119 #[test]
7120 fn charm_reinscription_in_same_tx_with_pointer() {
7121 let server = TestServer::builder().chain(Chain::Regtest).build();
7122
7123 server.mine_blocks(3);
7124
7125 let cursed_inscription = inscription("text/plain", "bar");
7126 let reinscription: Inscription = InscriptionTemplate {
7127 pointer: Some(0),
7128 ..default()
7129 }
7130 .into();
7131
7132 let txid = server.core.broadcast_tx(TransactionTemplate {
7133 inputs: &[
7134 (1, 0, 0, inscription("text/plain", "foo").to_witness()),
7135 (2, 0, 0, cursed_inscription.to_witness()),
7136 (3, 0, 0, reinscription.to_witness()),
7137 ],
7138 ..default()
7139 });
7140
7141 server.mine_blocks(1);
7142
7143 let id = InscriptionId { txid, index: 0 };
7144 server.assert_response_regex(
7145 format!("/inscription/{id}"),
7146 StatusCode::OK,
7147 format!(
7148 ".*<h1>Inscription 0</h1>.*
7149<dl>
7150 <dt>id</dt>
7151 <dd class=collapse>{id}</dd>
7152 .*
7153 <dt>value</dt>
7154 .*
7155</dl>
7156.*
7157"
7158 ),
7159 );
7160
7161 let id = InscriptionId { txid, index: 1 };
7162 server.assert_response_regex(
7163 format!("/inscription/{id}"),
7164 StatusCode::OK,
7165 ".*
7166 <span title=cursed>👹</span>.*",
7167 );
7168
7169 let id = InscriptionId { txid, index: 2 };
7170 server.assert_response_regex(
7171 format!("/inscription/{id}"),
7172 StatusCode::OK,
7173 ".*
7174 <span title=reinscription>♻️</span>
7175 <span title=cursed>👹</span>.*",
7176 );
7177 }
7178
7179 #[test]
7180 fn charm_unbound() {
7181 let server = TestServer::builder().chain(Chain::Regtest).build();
7182
7183 server.mine_blocks(1);
7184
7185 let txid = server.core.broadcast_tx(TransactionTemplate {
7186 inputs: &[(1, 0, 0, envelope(&[b"ord", &[128], &[0]]))],
7187 ..default()
7188 });
7189
7190 server.mine_blocks(1);
7191
7192 let id = InscriptionId { txid, index: 0 };
7193
7194 server.assert_response_regex(
7195 format!("/inscription/{id}"),
7196 StatusCode::OK,
7197 format!(
7198 ".*<h1>Inscription -1</h1>.*
7199<dl>
7200 <dt>id</dt>
7201 <dd class=collapse>{id}</dd>
7202 <dt>charms</dt>
7203 <dd>
7204 <span title=cursed>👹</span>
7205 <span title=unbound>🔓</span>
7206 </dd>
7207 .*
7208</dl>
7209.*
7210"
7211 ),
7212 );
7213 }
7214
7215 #[test]
7216 fn charm_lost() {
7217 let server = TestServer::builder().chain(Chain::Regtest).build();
7218
7219 server.mine_blocks(1);
7220
7221 let txid = server.core.broadcast_tx(TransactionTemplate {
7222 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
7223 ..default()
7224 });
7225
7226 let id = InscriptionId { txid, index: 0 };
7227
7228 server.mine_blocks(1);
7229
7230 server.assert_response_regex(
7231 format!("/inscription/{id}"),
7232 StatusCode::OK,
7233 format!(
7234 ".*<h1>Inscription 0</h1>.*
7235<dl>
7236 <dt>id</dt>
7237 <dd class=collapse>{id}</dd>
7238 .*
7239 <dt>value</dt>
7240 <dd>5000000000</dd>
7241 .*
7242</dl>
7243.*
7244"
7245 ),
7246 );
7247
7248 server.core.broadcast_tx(TransactionTemplate {
7249 inputs: &[(2, 1, 0, Default::default())],
7250 fee: 50 * COIN_VALUE,
7251 ..default()
7252 });
7253
7254 server.mine_blocks_with_subsidy(1, 0);
7255
7256 server.assert_response_regex(
7257 format!("/inscription/{id}"),
7258 StatusCode::OK,
7259 format!(
7260 ".*<h1>Inscription 0</h1>.*
7261<dl>
7262 <dt>id</dt>
7263 <dd class=collapse>{id}</dd>
7264 <dt>charms</dt>
7265 <dd>
7266 <span title=lost>🤔</span>
7267 </dd>
7268 .*
7269</dl>
7270.*
7271"
7272 ),
7273 );
7274 }
7275
7276 #[test]
7277 fn utxo_recursive_endpoint_all() {
7278 let server = TestServer::builder()
7279 .chain(Chain::Regtest)
7280 .index_sats()
7281 .index_runes()
7282 .build();
7283
7284 let rune = Rune(RUNE);
7285
7286 let (txid, id) = server.etch(
7287 Runestone {
7288 edicts: vec![Edict {
7289 id: RuneId::default(),
7290 amount: u128::MAX,
7291 output: 0,
7292 }],
7293 etching: Some(Etching {
7294 divisibility: Some(1),
7295 rune: Some(rune),
7296 premine: Some(u128::MAX),
7297 ..default()
7298 }),
7299 ..default()
7300 },
7301 1,
7302 None,
7303 );
7304
7305 pretty_assert_eq!(
7306 server.index.runes().unwrap(),
7307 [(
7308 id,
7309 RuneEntry {
7310 block: id.block,
7311 divisibility: 1,
7312 etching: txid,
7313 spaced_rune: SpacedRune { rune, spacers: 0 },
7314 premine: u128::MAX,
7315 timestamp: id.block,
7316 ..default()
7317 }
7318 )]
7319 );
7320
7321 server.mine_blocks(1);
7322
7323 let txid = server.core.broadcast_tx(TransactionTemplate {
7325 inputs: &[
7326 (6, 0, 0, inscription("text/plain", "foo").to_witness()),
7327 (7, 0, 0, inscription("text/plain", "bar").to_witness()),
7328 (7, 1, 0, Witness::new()),
7329 ],
7330 ..default()
7331 });
7332
7333 server.mine_blocks(1);
7334
7335 let inscription_id = InscriptionId { txid, index: 0 };
7336 let second_inscription_id = InscriptionId { txid, index: 1 };
7337 let outpoint: OutPoint = OutPoint { txid, vout: 0 };
7338
7339 let utxo_recursive = server.get_json::<api::UtxoRecursive>(format!("/r/utxo/{outpoint}"));
7340
7341 pretty_assert_eq!(
7342 utxo_recursive,
7343 api::UtxoRecursive {
7344 inscriptions: Some(vec![inscription_id, second_inscription_id]),
7345 runes: Some(
7346 [(
7347 SpacedRune { rune, spacers: 0 },
7348 Pile {
7349 amount: u128::MAX,
7350 divisibility: 1,
7351 symbol: None
7352 }
7353 )]
7354 .into_iter()
7355 .collect()
7356 ),
7357 sat_ranges: Some(vec![
7358 (6 * 50 * COIN_VALUE, 7 * 50 * COIN_VALUE),
7359 (7 * 50 * COIN_VALUE, 8 * 50 * COIN_VALUE),
7360 (50 * COIN_VALUE, 2 * 50 * COIN_VALUE)
7361 ]),
7362 value: 150 * COIN_VALUE,
7363 }
7364 );
7365 }
7366
7367 #[test]
7368 fn utxo_recursive_endpoint_only_inscriptions() {
7369 let server = TestServer::builder().chain(Chain::Regtest).build();
7370
7371 server.mine_blocks(1);
7372
7373 let txid = server.core.broadcast_tx(TransactionTemplate {
7374 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
7375 ..default()
7376 });
7377
7378 server.mine_blocks(1);
7379
7380 let inscription_id = InscriptionId { txid, index: 0 };
7381 let outpoint: OutPoint = OutPoint { txid, vout: 0 };
7382
7383 let utxo_recursive = server.get_json::<api::UtxoRecursive>(format!("/r/utxo/{outpoint}"));
7384
7385 pretty_assert_eq!(
7386 utxo_recursive,
7387 api::UtxoRecursive {
7388 inscriptions: Some(vec![inscription_id]),
7389 runes: None,
7390 sat_ranges: None,
7391 value: 50 * COIN_VALUE,
7392 }
7393 );
7394 }
7395
7396 #[test]
7397 fn sat_recursive_endpoints() {
7398 let server = TestServer::builder()
7399 .chain(Chain::Regtest)
7400 .index_sats()
7401 .build();
7402
7403 assert_eq!(
7404 server.get_json::<api::SatInscriptions>("/r/sat/5000000000"),
7405 api::SatInscriptions {
7406 ids: Vec::new(),
7407 page: 0,
7408 more: false
7409 }
7410 );
7411
7412 assert_eq!(
7413 server.get_json::<api::SatInscription>("/r/sat/5000000000/at/0"),
7414 api::SatInscription { id: None }
7415 );
7416
7417 server.mine_blocks(1);
7418
7419 let txid = server.core.broadcast_tx(TransactionTemplate {
7420 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
7421 ..default()
7422 });
7423
7424 server.mine_blocks(1);
7425
7426 let mut ids = Vec::new();
7427 ids.push(InscriptionId { txid, index: 0 });
7428
7429 for i in 1..111 {
7430 let txid = server.core.broadcast_tx(TransactionTemplate {
7431 inputs: &[(i + 1, 1, 0, inscription("text/plain", "foo").to_witness())],
7432 ..default()
7433 });
7434
7435 server.mine_blocks(1);
7436
7437 ids.push(InscriptionId { txid, index: 0 });
7438 }
7439
7440 let paginated_response = server.get_json::<api::SatInscriptions>("/r/sat/5000000000");
7441
7442 let equivalent_paginated_response =
7443 server.get_json::<api::SatInscriptions>("/r/sat/5000000000/0");
7444
7445 assert_eq!(paginated_response.ids.len(), 100);
7446 assert!(paginated_response.more);
7447 assert_eq!(paginated_response.page, 0);
7448
7449 assert_eq!(
7450 paginated_response.ids.len(),
7451 equivalent_paginated_response.ids.len()
7452 );
7453 assert_eq!(paginated_response.more, equivalent_paginated_response.more);
7454 assert_eq!(paginated_response.page, equivalent_paginated_response.page);
7455
7456 let paginated_response = server.get_json::<api::SatInscriptions>("/r/sat/5000000000/1");
7457
7458 assert_eq!(paginated_response.ids.len(), 11);
7459 assert!(!paginated_response.more);
7460 assert_eq!(paginated_response.page, 1);
7461
7462 assert_eq!(
7463 server
7464 .get_json::<api::SatInscription>("/r/sat/5000000000/at/0")
7465 .id,
7466 Some(ids[0])
7467 );
7468
7469 assert_eq!(
7470 server
7471 .get_json::<api::SatInscription>("/r/sat/5000000000/at/-111")
7472 .id,
7473 Some(ids[0])
7474 );
7475
7476 assert_eq!(
7477 server
7478 .get_json::<api::SatInscription>("/r/sat/5000000000/at/110")
7479 .id,
7480 Some(ids[110])
7481 );
7482
7483 assert_eq!(
7484 server
7485 .get_json::<api::SatInscription>("/r/sat/5000000000/at/-1")
7486 .id,
7487 Some(ids[110])
7488 );
7489
7490 assert!(
7491 server
7492 .get_json::<api::SatInscription>("/r/sat/5000000000/at/111")
7493 .id
7494 .is_none()
7495 );
7496
7497 assert_eq!(
7498 server
7499 .get("/r/sat/5000000000/at/0")
7500 .headers()
7501 .get(header::CACHE_CONTROL),
7502 None,
7503 );
7504
7505 assert_eq!(
7506 server
7507 .get("/r/sat/5000000000/at/0/content")
7508 .headers()
7509 .get(header::CACHE_CONTROL)
7510 .unwrap(),
7511 "public, max-age=1209600, immutable",
7512 );
7513
7514 assert_eq!(
7515 server
7516 .get("/r/sat/5000000000/at/-1")
7517 .headers()
7518 .get(header::CACHE_CONTROL)
7519 .unwrap(),
7520 "no-store",
7521 );
7522
7523 assert_eq!(
7524 server
7525 .get("/r/sat/5000000000/at/-1/content")
7526 .headers()
7527 .get(header::CACHE_CONTROL)
7528 .unwrap(),
7529 "no-store",
7530 );
7531 }
7532
7533 #[test]
7534 fn children_recursive_endpoint() {
7535 let server = TestServer::builder().chain(Chain::Regtest).build();
7536 server.mine_blocks(1);
7537
7538 let parent_txid = server.core.broadcast_tx(TransactionTemplate {
7539 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
7540 ..default()
7541 });
7542
7543 let parent_inscription_id = InscriptionId {
7544 txid: parent_txid,
7545 index: 0,
7546 };
7547
7548 server.assert_response(
7549 format!("/r/children/{parent_inscription_id}"),
7550 StatusCode::NOT_FOUND,
7551 &format!("inscription {parent_inscription_id} not found"),
7552 );
7553
7554 server.mine_blocks(1);
7555
7556 let children_json =
7557 server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}"));
7558 assert_eq!(children_json.ids.len(), 0);
7559
7560 let mut builder = script::Builder::new();
7561 for _ in 0..111 {
7562 builder = Inscription {
7563 content_type: Some("text/plain".into()),
7564 body: Some("hello".into()),
7565 parents: vec![parent_inscription_id.value()],
7566 unrecognized_even_field: false,
7567 ..default()
7568 }
7569 .append_reveal_script_to_builder(builder);
7570 }
7571
7572 let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);
7573
7574 let txid = server.core.broadcast_tx(TransactionTemplate {
7575 inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
7576 ..default()
7577 });
7578
7579 server.mine_blocks(1);
7580
7581 let first_child_inscription_id = InscriptionId { txid, index: 0 };
7582 let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
7583 let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
7584 let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };
7585
7586 let children_json =
7587 server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}"));
7588
7589 assert_eq!(children_json.ids.len(), 100);
7590 assert_eq!(children_json.ids[0], first_child_inscription_id);
7591 assert_eq!(children_json.ids[99], hundredth_child_inscription_id);
7592 assert!(children_json.more);
7593 assert_eq!(children_json.page, 0);
7594
7595 let children_json =
7596 server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}/1"));
7597
7598 assert_eq!(children_json.ids.len(), 11);
7599 assert_eq!(children_json.ids[0], hundred_first_child_inscription_id);
7600 assert_eq!(children_json.ids[10], hundred_eleventh_child_inscription_id);
7601 assert!(!children_json.more);
7602 assert_eq!(children_json.page, 1);
7603 }
7604
7605 #[test]
7606 fn children_json_endpoint() {
7607 let server = TestServer::builder().chain(Chain::Regtest).build();
7608 server.mine_blocks(1);
7609
7610 let parent_txid = server.core.broadcast_tx(TransactionTemplate {
7611 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
7612 ..default()
7613 });
7614
7615 let parent_inscription_id = InscriptionId {
7616 txid: parent_txid,
7617 index: 0,
7618 };
7619
7620 server.assert_response(
7621 format!("/children/{parent_inscription_id}"),
7622 StatusCode::NOT_FOUND,
7623 &format!("inscription {parent_inscription_id} not found"),
7624 );
7625
7626 server.mine_blocks(1);
7627
7628 let children_json =
7629 server.get_json::<api::Children>(format!("/children/{parent_inscription_id}"));
7630 assert_eq!(children_json.ids.len(), 0);
7631 assert!(!children_json.more);
7632 assert_eq!(children_json.page, 0);
7633
7634 let mut builder = script::Builder::new();
7635 for _ in 0..111 {
7636 builder = Inscription {
7637 content_type: Some("text/plain".into()),
7638 body: Some("hello".into()),
7639 parents: vec![parent_inscription_id.value()],
7640 unrecognized_even_field: false,
7641 ..default()
7642 }
7643 .append_reveal_script_to_builder(builder);
7644 }
7645
7646 let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);
7647
7648 let txid = server.core.broadcast_tx(TransactionTemplate {
7649 inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
7650 ..default()
7651 });
7652
7653 server.mine_blocks(1);
7654
7655 let first_child_inscription_id = InscriptionId { txid, index: 0 };
7656 let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
7657 let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
7658 let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };
7659
7660 let children_json =
7661 server.get_json::<api::Children>(format!("/children/{parent_inscription_id}"));
7662
7663 assert_eq!(children_json.ids.len(), 100);
7664 assert_eq!(children_json.ids[0], first_child_inscription_id);
7665 assert_eq!(children_json.ids[99], hundredth_child_inscription_id);
7666 assert!(children_json.more);
7667 assert_eq!(children_json.page, 0);
7668
7669 let children_json =
7670 server.get_json::<api::Children>(format!("/children/{parent_inscription_id}/1"));
7671
7672 assert_eq!(children_json.ids.len(), 11);
7673 assert_eq!(children_json.ids[0], hundred_first_child_inscription_id);
7674 assert_eq!(children_json.ids[10], hundred_eleventh_child_inscription_id);
7675 assert!(!children_json.more);
7676 assert_eq!(children_json.page, 1);
7677 }
7678
7679 #[test]
7680 fn parents_recursive_endpoint() {
7681 let server = TestServer::builder().chain(Chain::Regtest).build();
7682 server.mine_blocks(1);
7683
7684 let mut parent_ids = Vec::new();
7685 let mut inputs = Vec::new();
7686 for i in 0..111 {
7687 parent_ids.push(InscriptionId {
7688 txid: server.core.broadcast_tx(TransactionTemplate {
7689 inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
7690 ..default()
7691 }),
7692 index: 0,
7693 });
7694
7695 inputs.push((i + 2, 1, 0, Witness::default()));
7696
7697 server.mine_blocks(1);
7698 }
7699
7700 inputs.insert(
7701 0,
7702 (
7703 112,
7704 0,
7705 0,
7706 Inscription {
7707 content_type: Some("text/plain".into()),
7708 body: Some("hello".into()),
7709 parents: parent_ids.iter().map(|id| id.value()).collect(),
7710 ..default()
7711 }
7712 .to_witness(),
7713 ),
7714 );
7715
7716 let txid = server.core.broadcast_tx(TransactionTemplate {
7717 inputs: &inputs,
7718 ..default()
7719 });
7720
7721 server.mine_blocks(1);
7722
7723 let inscription_id = InscriptionId { txid, index: 0 };
7724
7725 let first_parent_inscription_id = parent_ids[0];
7726 let hundredth_parent_inscription_id = parent_ids[99];
7727 let hundred_first_parent_inscription_id = parent_ids[100];
7728 let hundred_eleventh_parent_inscription_id = parent_ids[110];
7729
7730 let parents_json = server.get_json::<api::Inscriptions>(format!("/r/parents/{inscription_id}"));
7731
7732 assert_eq!(parents_json.ids.len(), 100);
7733 assert_eq!(parents_json.ids[0], first_parent_inscription_id);
7734 assert_eq!(parents_json.ids[99], hundredth_parent_inscription_id);
7735 assert!(parents_json.more);
7736 assert_eq!(parents_json.page_index, 0);
7737
7738 let parents_json =
7739 server.get_json::<api::Inscriptions>(format!("/r/parents/{inscription_id}/1"));
7740
7741 assert_eq!(parents_json.ids.len(), 11);
7742 assert_eq!(parents_json.ids[0], hundred_first_parent_inscription_id);
7743 assert_eq!(parents_json.ids[10], hundred_eleventh_parent_inscription_id);
7744 assert!(!parents_json.more);
7745 assert_eq!(parents_json.page_index, 1);
7746 }
7747
7748 #[test]
7749 fn child_inscriptions_recursive_endpoint() {
7750 let server = TestServer::builder().chain(Chain::Regtest).build();
7751 server.mine_blocks(1);
7752
7753 let parent_txid = server.core.broadcast_tx(TransactionTemplate {
7754 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
7755 ..default()
7756 });
7757
7758 let parent_inscription_id = InscriptionId {
7759 txid: parent_txid,
7760 index: 0,
7761 };
7762
7763 server.assert_response(
7764 format!("/r/children/{parent_inscription_id}/inscriptions"),
7765 StatusCode::NOT_FOUND,
7766 &format!("inscription {parent_inscription_id} not found"),
7767 );
7768
7769 server.mine_blocks(1);
7770
7771 let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
7772 "/r/children/{parent_inscription_id}/inscriptions"
7773 ));
7774 assert_eq!(child_inscriptions_json.children.len(), 0);
7775
7776 let mut builder = script::Builder::new();
7777 for _ in 0..111 {
7778 builder = Inscription {
7779 content_type: Some("text/plain".into()),
7780 body: Some("hello".into()),
7781 parents: vec![parent_inscription_id.value()],
7782 unrecognized_even_field: false,
7783 ..default()
7784 }
7785 .append_reveal_script_to_builder(builder);
7786 }
7787
7788 let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);
7789
7790 let txid = server.core.broadcast_tx(TransactionTemplate {
7791 inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
7792 ..default()
7793 });
7794
7795 server.mine_blocks(1);
7796
7797 let first_child_inscription_id = InscriptionId { txid, index: 0 };
7798 let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
7799 let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
7800 let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };
7801
7802 let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
7803 "/r/children/{parent_inscription_id}/inscriptions"
7804 ));
7805
7806 assert_eq!(child_inscriptions_json.children.len(), 100);
7807
7808 assert_eq!(
7809 child_inscriptions_json.children[0].id,
7810 first_child_inscription_id
7811 );
7812 assert_eq!(child_inscriptions_json.children[0].number, 1); assert_eq!(
7815 child_inscriptions_json.children[99].id,
7816 hundredth_child_inscription_id
7817 );
7818 assert_eq!(child_inscriptions_json.children[99].number, -99); assert!(child_inscriptions_json.more);
7821 assert_eq!(child_inscriptions_json.page, 0);
7822
7823 let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
7824 "/r/children/{parent_inscription_id}/inscriptions/1"
7825 ));
7826
7827 assert_eq!(child_inscriptions_json.children.len(), 11);
7828
7829 assert_eq!(
7830 child_inscriptions_json.children[0].id,
7831 hundred_first_child_inscription_id
7832 );
7833 assert_eq!(child_inscriptions_json.children[0].number, -100);
7834
7835 assert_eq!(
7836 child_inscriptions_json.children[10].id,
7837 hundred_eleventh_child_inscription_id
7838 );
7839 assert_eq!(child_inscriptions_json.children[10].number, -110);
7840
7841 assert!(!child_inscriptions_json.more);
7842 assert_eq!(child_inscriptions_json.page, 1);
7843 }
7844
7845 #[test]
7846 fn parent_inscriptions_recursive_endpoint() {
7847 let server = TestServer::builder().chain(Chain::Regtest).build();
7848 server.mine_blocks(1);
7849
7850 let mut builder = script::Builder::new();
7851 for _ in 0..111 {
7852 builder = Inscription {
7853 content_type: Some("text/plain".into()),
7854 body: Some("hello".into()),
7855 unrecognized_even_field: false,
7856 ..default()
7857 }
7858 .append_reveal_script_to_builder(builder);
7859 }
7860
7861 let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);
7862
7863 let parents_txid = server.core.broadcast_tx(TransactionTemplate {
7864 inputs: &[(1, 0, 0, witness)],
7865 ..default()
7866 });
7867
7868 server.mine_blocks(1);
7869
7870 let mut builder = script::Builder::new();
7871 builder = Inscription {
7872 content_type: Some("text/plain".into()),
7873 body: Some("hello".into()),
7874 parents: (0..111)
7875 .map(|i| {
7876 InscriptionId {
7877 txid: parents_txid,
7878 index: i,
7879 }
7880 .value()
7881 })
7882 .collect(),
7883 unrecognized_even_field: false,
7884 ..default()
7885 }
7886 .append_reveal_script_to_builder(builder);
7887
7888 let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);
7889
7890 let child_txid = server.core.broadcast_tx(TransactionTemplate {
7891 inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
7892 ..default()
7893 });
7894
7895 let child_inscription_id = InscriptionId {
7896 txid: child_txid,
7897 index: 0,
7898 };
7899
7900 server.assert_response(
7901 format!("/r/parents/{child_inscription_id}/inscriptions"),
7902 StatusCode::NOT_FOUND,
7903 &format!("inscription {child_inscription_id} not found"),
7904 );
7905
7906 server.mine_blocks(1);
7907
7908 let first_parent_inscription_id = InscriptionId {
7909 txid: parents_txid,
7910 index: 0,
7911 };
7912 let hundredth_parent_inscription_id = InscriptionId {
7913 txid: parents_txid,
7914 index: 99,
7915 };
7916 let hundred_first_parent_inscription_id = InscriptionId {
7917 txid: parents_txid,
7918 index: 100,
7919 };
7920 let hundred_eleventh_parent_inscription_id = InscriptionId {
7921 txid: parents_txid,
7922 index: 110,
7923 };
7924
7925 let parent_inscriptions_json = server.get_json::<api::ParentInscriptions>(format!(
7926 "/r/parents/{child_inscription_id}/inscriptions"
7927 ));
7928
7929 assert_eq!(parent_inscriptions_json.parents.len(), 100);
7930
7931 assert_eq!(
7932 parent_inscriptions_json.parents[0].id,
7933 first_parent_inscription_id
7934 );
7935 assert_eq!(parent_inscriptions_json.parents[0].number, 0); assert_eq!(
7938 parent_inscriptions_json.parents[99].id,
7939 hundredth_parent_inscription_id
7940 );
7941 assert_eq!(parent_inscriptions_json.parents[99].number, -99); assert!(parent_inscriptions_json.more);
7944 assert_eq!(parent_inscriptions_json.page, 0);
7945
7946 let parent_inscriptions_json = server.get_json::<api::ParentInscriptions>(format!(
7947 "/r/parents/{child_inscription_id}/inscriptions/1"
7948 ));
7949
7950 assert_eq!(parent_inscriptions_json.parents.len(), 11);
7951
7952 assert_eq!(
7953 parent_inscriptions_json.parents[0].id,
7954 hundred_first_parent_inscription_id
7955 );
7956 assert_eq!(parent_inscriptions_json.parents[0].number, -100);
7957
7958 assert_eq!(
7959 parent_inscriptions_json.parents[10].id,
7960 hundred_eleventh_parent_inscription_id
7961 );
7962 assert_eq!(parent_inscriptions_json.parents[10].number, -110);
7963
7964 assert!(!parent_inscriptions_json.more);
7965 assert_eq!(parent_inscriptions_json.page, 1);
7966 }
7967
7968 #[test]
7969 fn inscriptions_in_block_page() {
7970 let server = TestServer::builder()
7971 .chain(Chain::Regtest)
7972 .index_sats()
7973 .build();
7974
7975 for _ in 0..101 {
7976 server.mine_blocks(1);
7977 }
7978
7979 for i in 0..101 {
7980 server.core.broadcast_tx(TransactionTemplate {
7981 inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
7982 ..default()
7983 });
7984 }
7985
7986 server.mine_blocks(1);
7987
7988 server.assert_response_regex(
7989 "/inscriptions/block/102",
7990 StatusCode::OK,
7991 r".*(<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>.*){100}.*",
7992 );
7993
7994 server.assert_response_regex(
7995 "/inscriptions/block/102/1",
7996 StatusCode::OK,
7997 r".*<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>.*",
7998 );
7999
8000 server.assert_response_regex(
8001 "/inscriptions/block/102/2",
8002 StatusCode::OK,
8003 r".*<div class=thumbnails>\s*</div>.*",
8004 );
8005 }
8006
8007 #[test]
8008 fn inscription_query_display() {
8009 assert_eq!(
8010 query::Inscription::Id(inscription_id(1)).to_string(),
8011 "1111111111111111111111111111111111111111111111111111111111111111i1"
8012 );
8013 assert_eq!(query::Inscription::Number(1).to_string(), "1")
8014 }
8015
8016 #[test]
8017 fn inscription_not_found() {
8018 TestServer::builder()
8019 .chain(Chain::Regtest)
8020 .build()
8021 .assert_response(
8022 "/inscription/0",
8023 StatusCode::NOT_FOUND,
8024 "inscription 0 not found",
8025 );
8026 }
8027
8028 #[test]
8029 fn looking_up_inscription_by_sat_requires_sat_index() {
8030 TestServer::builder()
8031 .chain(Chain::Regtest)
8032 .build()
8033 .assert_response(
8034 "/inscription/abcd",
8035 StatusCode::NOT_FOUND,
8036 "sat index required",
8037 );
8038 }
8039
8040 #[test]
8041 fn delegate() {
8042 let server = TestServer::builder().chain(Chain::Regtest).build();
8043
8044 server.mine_blocks(1);
8045
8046 let delegate = Inscription {
8047 content_type: Some("text/html".into()),
8048 body: Some("foo".into()),
8049 ..default()
8050 };
8051
8052 let txid = server.core.broadcast_tx(TransactionTemplate {
8053 inputs: &[(1, 0, 0, delegate.to_witness())],
8054 ..default()
8055 });
8056
8057 let delegate = InscriptionId { txid, index: 0 };
8058
8059 server.mine_blocks(1);
8060
8061 let inscription = Inscription {
8062 delegate: Some(delegate.value()),
8063 ..default()
8064 };
8065
8066 let txid = server.core.broadcast_tx(TransactionTemplate {
8067 inputs: &[(2, 0, 0, inscription.to_witness())],
8068 ..default()
8069 });
8070
8071 server.mine_blocks(1);
8072
8073 let id = InscriptionId { txid, index: 0 };
8074
8075 server.assert_response_regex(
8076 format!("/inscription/{id}"),
8077 StatusCode::OK,
8078 format!(
8079 ".*<h1>Inscription 1</h1>.*
8080 <dl>
8081 <dt>id</dt>
8082 <dd class=collapse>{id}</dd>
8083 .*
8084 <dt>delegate</dt>
8085 <dd><a href=/inscription/{delegate}>{delegate}</a></dd>
8086 .*
8087 </dl>.*"
8088 )
8089 .unindent(),
8090 );
8091
8092 server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
8093
8094 server.assert_response(format!("/preview/{id}"), StatusCode::OK, "foo");
8095
8096 assert_eq!(
8097 server
8098 .get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}"))
8099 .delegate,
8100 Some(delegate)
8101 );
8102 }
8103
8104 #[test]
8105 fn undelegated_content() {
8106 let server = TestServer::builder().chain(Chain::Regtest).build();
8107
8108 server.mine_blocks(1);
8109
8110 let delegate = Inscription {
8111 content_type: Some("text/plain".into()),
8112 body: Some("foo".into()),
8113 ..default()
8114 };
8115
8116 let delegate_txid = server.core.broadcast_tx(TransactionTemplate {
8117 inputs: &[(1, 0, 0, delegate.to_witness())],
8118 ..default()
8119 });
8120
8121 let delegate_id = InscriptionId {
8122 txid: delegate_txid,
8123 index: 0,
8124 };
8125
8126 server.mine_blocks(1);
8127
8128 let inscription = Inscription {
8129 content_type: Some("text/plain".into()),
8130 body: Some("bar".into()),
8131 delegate: Some(delegate_id.value()),
8132 ..default()
8133 };
8134
8135 let txid = server.core.broadcast_tx(TransactionTemplate {
8136 inputs: &[(2, 0, 0, inscription.to_witness())],
8137 ..default()
8138 });
8139
8140 server.mine_blocks(1);
8141
8142 let id = InscriptionId { txid, index: 0 };
8143
8144 server.assert_response(
8145 format!("/r/undelegated-content/{id}"),
8146 StatusCode::OK,
8147 "bar",
8148 );
8149
8150 server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
8151
8152 let normal_inscription = Inscription {
8154 content_type: Some("text/plain".into()),
8155 body: Some("baz".into()),
8156 ..default()
8157 };
8158
8159 let normal_txid = server.core.broadcast_tx(TransactionTemplate {
8160 inputs: &[(3, 0, 0, normal_inscription.to_witness())],
8161 ..default()
8162 });
8163
8164 server.mine_blocks(1);
8165
8166 let normal_id = InscriptionId {
8167 txid: normal_txid,
8168 index: 0,
8169 };
8170
8171 server.assert_response(
8172 format!("/r/undelegated-content/{normal_id}"),
8173 StatusCode::OK,
8174 "baz",
8175 );
8176 server.assert_response(format!("/content/{normal_id}"), StatusCode::OK, "baz");
8177 }
8178
8179 #[test]
8180 fn content_proxy() {
8181 let server = TestServer::builder().chain(Chain::Regtest).build();
8182
8183 server.mine_blocks(1);
8184
8185 let inscription = Inscription {
8186 content_type: Some("text/html".into()),
8187 body: Some("foo".into()),
8188 ..default()
8189 };
8190
8191 let txid = server.core.broadcast_tx(TransactionTemplate {
8192 inputs: &[(1, 0, 0, inscription.to_witness())],
8193 ..default()
8194 });
8195
8196 server.mine_blocks(1);
8197
8198 let id = InscriptionId { txid, index: 0 };
8199
8200 server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
8201
8202 let server_with_proxy = TestServer::builder()
8203 .chain(Chain::Regtest)
8204 .server_option("--proxy", server.url.as_ref())
8205 .build();
8206
8207 server_with_proxy.mine_blocks(1);
8208
8209 server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
8210 server_with_proxy.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
8211 }
8212
8213 #[test]
8214 fn metadata_proxy() {
8215 let server = TestServer::builder().chain(Chain::Regtest).build();
8216
8217 server.mine_blocks(1);
8218
8219 let mut metadata = Vec::new();
8220 ciborium::into_writer("bar", &mut metadata).unwrap();
8221
8222 let inscription = Inscription {
8223 content_type: Some("text/html".into()),
8224 body: Some("foo".into()),
8225 metadata: Some(metadata.clone()),
8226 ..default()
8227 };
8228
8229 let txid = server.core.broadcast_tx(TransactionTemplate {
8230 inputs: &[(1, 0, 0, inscription.to_witness())],
8231 ..default()
8232 });
8233
8234 server.mine_blocks(1);
8235
8236 let id = InscriptionId { txid, index: 0 };
8237
8238 server.assert_response(
8239 format!("/r/metadata/{id}"),
8240 StatusCode::OK,
8241 &format!("\"{}\"", hex::encode(metadata.clone())),
8242 );
8243
8244 let server_with_proxy = TestServer::builder()
8245 .chain(Chain::Regtest)
8246 .server_option("--proxy", server.url.as_ref())
8247 .build();
8248
8249 server_with_proxy.mine_blocks(1);
8250
8251 server.assert_response(
8252 format!("/r/metadata/{id}"),
8253 StatusCode::OK,
8254 &format!("\"{}\"", hex::encode(metadata.clone())),
8255 );
8256
8257 server_with_proxy.assert_response(
8258 format!("/r/metadata/{id}"),
8259 StatusCode::OK,
8260 &format!("\"{}\"", hex::encode(metadata.clone())),
8261 );
8262 }
8263
8264 #[test]
8265 fn children_proxy() {
8266 let server = TestServer::builder().chain(Chain::Regtest).build();
8267
8268 server.mine_blocks(1);
8269
8270 let parent_txid = server.core.broadcast_tx(TransactionTemplate {
8271 inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
8272 ..default()
8273 });
8274
8275 let parent_id = InscriptionId {
8276 txid: parent_txid,
8277 index: 0,
8278 };
8279
8280 server.assert_response(
8281 format!("/r/children/{parent_id}"),
8282 StatusCode::NOT_FOUND,
8283 &format!("inscription {parent_id} not found"),
8284 );
8285
8286 server.mine_blocks(1);
8287
8288 let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));
8289
8290 assert_eq!(children.ids.len(), 0);
8291
8292 let mut builder = script::Builder::new();
8293 for _ in 0..11 {
8294 builder = Inscription {
8295 content_type: Some("text/plain".into()),
8296 body: Some("hello".into()),
8297 parents: vec![parent_id.value()],
8298 unrecognized_even_field: false,
8299 ..default()
8300 }
8301 .append_reveal_script_to_builder(builder);
8302 }
8303
8304 let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);
8305
8306 let txid = server.core.broadcast_tx(TransactionTemplate {
8307 inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
8308 ..default()
8309 });
8310
8311 server.mine_blocks(1);
8312
8313 let first_child_id = InscriptionId { txid, index: 0 };
8314
8315 let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));
8316
8317 assert_eq!(children.ids.len(), 11);
8318 assert_eq!(first_child_id, children.ids[0]);
8319
8320 let server_with_proxy = TestServer::builder()
8321 .chain(Chain::Regtest)
8322 .server_option("--proxy", server.url.as_ref())
8323 .build();
8324
8325 server_with_proxy.mine_blocks(1);
8326
8327 let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));
8328
8329 assert_eq!(children.ids.len(), 11);
8330 assert_eq!(first_child_id, children.ids[0]);
8331
8332 let children = server_with_proxy.get_json::<api::Children>(format!("/r/children/{parent_id}"));
8333
8334 assert_eq!(children.ids.len(), 11);
8335 assert_eq!(first_child_id, children.ids[0]);
8336 }
8337
8338 #[test]
8339 fn inscription_proxy() {
8340 let server = TestServer::builder().chain(Chain::Regtest).build();
8341
8342 server.mine_blocks(1);
8343
8344 let inscription = Inscription {
8345 content_type: Some("text/html".into()),
8346 body: Some("foo".into()),
8347 ..default()
8348 };
8349
8350 let txid = server.core.broadcast_tx(TransactionTemplate {
8351 inputs: &[(1, 0, 0, inscription.to_witness())],
8352 ..default()
8353 });
8354
8355 server.mine_blocks(1);
8356
8357 let id = InscriptionId { txid, index: 0 };
8358
8359 pretty_assert_eq!(
8360 server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
8361 api::InscriptionRecursive {
8362 charms: Vec::new(),
8363 content_type: Some("text/html".into()),
8364 content_length: Some(3),
8365 delegate: None,
8366 fee: 0,
8367 height: 2,
8368 id,
8369 number: 0,
8370 output: OutPoint { txid, vout: 0 },
8371 sat: None,
8372 satpoint: SatPoint {
8373 outpoint: OutPoint { txid, vout: 0 },
8374 offset: 0
8375 },
8376 timestamp: 2,
8377 value: Some(50 * COIN_VALUE),
8378 address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
8379 }
8380 );
8381
8382 let server_with_proxy = TestServer::builder()
8383 .chain(Chain::Regtest)
8384 .server_option("--proxy", server.url.as_ref())
8385 .build();
8386
8387 server_with_proxy.mine_blocks(1);
8388
8389 pretty_assert_eq!(
8390 server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
8391 api::InscriptionRecursive {
8392 charms: Vec::new(),
8393 content_type: Some("text/html".into()),
8394 content_length: Some(3),
8395 delegate: None,
8396 fee: 0,
8397 height: 2,
8398 id,
8399 number: 0,
8400 output: OutPoint { txid, vout: 0 },
8401 sat: None,
8402 satpoint: SatPoint {
8403 outpoint: OutPoint { txid, vout: 0 },
8404 offset: 0
8405 },
8406 timestamp: 2,
8407 value: Some(50 * COIN_VALUE),
8408 address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
8409 }
8410 );
8411
8412 assert_eq!(
8413 server_with_proxy.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
8414 api::InscriptionRecursive {
8415 charms: Vec::new(),
8416 content_type: Some("text/html".into()),
8417 content_length: Some(3),
8418 delegate: None,
8419 fee: 0,
8420 height: 2,
8421 id,
8422 number: 0,
8423 output: OutPoint { txid, vout: 0 },
8424 sat: None,
8425 satpoint: SatPoint {
8426 outpoint: OutPoint { txid, vout: 0 },
8427 offset: 0
8428 },
8429 timestamp: 2,
8430 value: Some(50 * COIN_VALUE),
8431 address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
8432 }
8433 );
8434 }
8435
8436 #[test]
8437 fn sat_at_index_proxy() {
8438 let server = TestServer::builder()
8439 .index_sats()
8440 .chain(Chain::Regtest)
8441 .build();
8442
8443 server.mine_blocks(1);
8444
8445 let inscription = Inscription {
8446 content_type: Some("text/html".into()),
8447 body: Some("foo".into()),
8448 ..default()
8449 };
8450
8451 let txid = server.core.broadcast_tx(TransactionTemplate {
8452 inputs: &[(1, 0, 0, inscription.to_witness())],
8453 ..default()
8454 });
8455
8456 server.mine_blocks(1);
8457
8458 let id = InscriptionId { txid, index: 0 };
8459 let ordinal: u64 = 5000000000;
8460
8461 pretty_assert_eq!(
8462 server.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
8463 api::SatInscription { id: Some(id) }
8464 );
8465
8466 let server_with_proxy = TestServer::builder()
8467 .chain(Chain::Regtest)
8468 .server_option("--proxy", server.url.as_ref())
8469 .build();
8470 let sat_indexed_server_with_proxy = TestServer::builder()
8471 .index_sats()
8472 .chain(Chain::Regtest)
8473 .server_option("--proxy", server.url.as_ref())
8474 .build();
8475
8476 server_with_proxy.mine_blocks(1);
8477 sat_indexed_server_with_proxy.mine_blocks(1);
8478
8479 pretty_assert_eq!(
8480 server.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
8481 api::SatInscription { id: Some(id) }
8482 );
8483
8484 pretty_assert_eq!(
8485 server_with_proxy.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
8486 api::SatInscription { id: Some(id) }
8487 );
8488
8489 pretty_assert_eq!(
8490 sat_indexed_server_with_proxy
8491 .get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
8492 api::SatInscription { id: Some(id) }
8493 );
8494 }
8495
8496 #[test]
8497 fn sat_at_index_content_proxy() {
8498 let server = TestServer::builder()
8499 .index_sats()
8500 .chain(Chain::Regtest)
8501 .build();
8502
8503 server.mine_blocks(1);
8504
8505 let inscription = Inscription {
8506 content_type: Some("text/html".into()),
8507 body: Some("foo".into()),
8508 ..default()
8509 };
8510
8511 let txid = server.core.broadcast_tx(TransactionTemplate {
8512 inputs: &[(1, 0, 0, inscription.to_witness())],
8513 ..default()
8514 });
8515
8516 server.mine_blocks(1);
8517
8518 let id = InscriptionId { txid, index: 0 };
8519 let ordinal: u64 = 5000000000;
8520
8521 pretty_assert_eq!(
8522 server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
8523 api::InscriptionRecursive {
8524 charms: vec![Charm::Coin, Charm::Uncommon],
8525 content_type: Some("text/html".into()),
8526 content_length: Some(3),
8527 delegate: None,
8528 fee: 0,
8529 height: 2,
8530 id,
8531 number: 0,
8532 output: OutPoint { txid, vout: 0 },
8533 sat: Some(Sat(ordinal)),
8534 satpoint: SatPoint {
8535 outpoint: OutPoint { txid, vout: 0 },
8536 offset: 0
8537 },
8538 timestamp: 2,
8539 value: Some(50 * COIN_VALUE),
8540 address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
8541 }
8542 );
8543
8544 server.assert_response(
8545 format!("/r/sat/{ordinal}/at/-1/content"),
8546 StatusCode::OK,
8547 "foo",
8548 );
8549
8550 let server_with_proxy = TestServer::builder()
8551 .chain(Chain::Regtest)
8552 .server_option("--proxy", server.url.as_ref())
8553 .build();
8554 let sat_indexed_server_with_proxy = TestServer::builder()
8555 .index_sats()
8556 .chain(Chain::Regtest)
8557 .server_option("--proxy", server.url.as_ref())
8558 .build();
8559
8560 server_with_proxy.mine_blocks(1);
8561 sat_indexed_server_with_proxy.mine_blocks(1);
8562
8563 server.assert_response(
8564 format!("/r/sat/{ordinal}/at/-1/content"),
8565 StatusCode::OK,
8566 "foo",
8567 );
8568 server_with_proxy.assert_response(
8569 format!("/r/sat/{ordinal}/at/-1/content"),
8570 StatusCode::OK,
8571 "foo",
8572 );
8573 sat_indexed_server_with_proxy.assert_response(
8574 format!("/r/sat/{ordinal}/at/-1/content"),
8575 StatusCode::OK,
8576 "foo",
8577 );
8578 }
8579
8580 #[test]
8581 fn block_info() {
8582 let server = TestServer::new();
8583
8584 pretty_assert_eq!(
8585 server.get_json::<api::BlockInfo>("/r/blockinfo/0"),
8586 api::BlockInfo {
8587 average_fee: 0,
8588 average_fee_rate: 0,
8589 bits: 486604799,
8590 chainwork: [0; 32],
8591 confirmations: 0,
8592 difficulty: 0.0,
8593 hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
8594 .parse()
8595 .unwrap(),
8596 feerate_percentiles: [0, 0, 0, 0, 0],
8597 height: 0,
8598 max_fee: 0,
8599 max_fee_rate: 0,
8600 max_tx_size: 0,
8601 median_fee: 0,
8602 median_time: None,
8603 merkle_root: TxMerkleNode::all_zeros(),
8604 min_fee: 0,
8605 min_fee_rate: 0,
8606 next_block: None,
8607 nonce: 0,
8608 previous_block: None,
8609 subsidy: 0,
8610 target: "00000000ffff0000000000000000000000000000000000000000000000000000"
8611 .parse()
8612 .unwrap(),
8613 timestamp: 0,
8614 total_fee: 0,
8615 total_size: 0,
8616 total_weight: 0,
8617 transaction_count: 0,
8618 version: 1,
8619 },
8620 );
8621
8622 server.mine_blocks(1);
8623
8624 pretty_assert_eq!(
8625 server.get_json::<api::BlockInfo>("/r/blockinfo/1"),
8626 api::BlockInfo {
8627 average_fee: 0,
8628 average_fee_rate: 0,
8629 bits: 0,
8630 chainwork: [0; 32],
8631 confirmations: 0,
8632 difficulty: 0.0,
8633 hash: "56d05060a0280d0712d113f25321158747310ece87ea9e299bde06cf385b8d85"
8634 .parse()
8635 .unwrap(),
8636 feerate_percentiles: [0, 0, 0, 0, 0],
8637 height: 1,
8638 max_fee: 0,
8639 max_fee_rate: 0,
8640 max_tx_size: 0,
8641 median_fee: 0,
8642 median_time: None,
8643 merkle_root: TxMerkleNode::all_zeros(),
8644 min_fee: 0,
8645 min_fee_rate: 0,
8646 next_block: None,
8647 nonce: 0,
8648 previous_block: None,
8649 subsidy: 0,
8650 target: BlockHash::all_zeros(),
8651 timestamp: 0,
8652 total_fee: 0,
8653 total_size: 0,
8654 total_weight: 0,
8655 transaction_count: 0,
8656 version: 1,
8657 },
8658 )
8659 }
8660
8661 #[test]
8662 fn authentication_requires_username_and_password() {
8663 assert!(Arguments::try_parse_from(["ord", "--server-username", "server", "foo"]).is_err());
8664 assert!(Arguments::try_parse_from(["ord", "--server-password", "server", "bar"]).is_err());
8665 assert!(
8666 Arguments::try_parse_from([
8667 "ord",
8668 "--server-username",
8669 "foo",
8670 "--server-password",
8671 "bar",
8672 "server"
8673 ])
8674 .is_ok()
8675 );
8676 }
8677
8678 #[test]
8679 fn inscriptions_can_be_hidden_with_config() {
8680 let core = mockcore::builder()
8681 .network(Chain::Regtest.network())
8682 .build();
8683
8684 core.mine_blocks(1);
8685
8686 let txid = core.broadcast_tx(TransactionTemplate {
8687 inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
8688 ..default()
8689 });
8690
8691 core.mine_blocks(1);
8692
8693 let inscription = InscriptionId { txid, index: 0 };
8694
8695 let server = TestServer::builder()
8696 .core(core)
8697 .config(&format!("hidden: [{inscription}]"))
8698 .build();
8699
8700 server.assert_response_regex(format!("/inscription/{inscription}"), StatusCode::OK, ".*");
8701
8702 server.assert_response_regex(
8703 format!("/content/{inscription}"),
8704 StatusCode::OK,
8705 PreviewUnknownHtml.to_string(),
8706 );
8707 }
8708
8709 #[test]
8710 fn update_endpoint_is_not_available_when_not_in_integration_test_mode() {
8711 let server = TestServer::builder().build();
8712 server.assert_response("/update", StatusCode::NOT_FOUND, "");
8713 }
8714
8715 #[test]
8716 fn burned_charm() {
8717 let server = TestServer::builder().chain(Chain::Regtest).build();
8718
8719 server.mine_blocks(1);
8720
8721 let inscription = Inscription {
8722 content_type: Some("text/html".into()),
8723 body: Some("foo".into()),
8724 ..default()
8725 };
8726
8727 let txid = server.core.broadcast_tx(TransactionTemplate {
8728 inputs: &[(1, 0, 0, inscription.to_witness())],
8729 outputs: 0,
8730 op_return_index: Some(0),
8731 op_return_value: Some(50 * COIN_VALUE),
8732 op_return: Some(
8733 script::Builder::new()
8734 .push_opcode(opcodes::all::OP_RETURN)
8735 .into_script(),
8736 ),
8737 ..default()
8738 });
8739
8740 server.mine_blocks(1);
8741
8742 let id = InscriptionId { txid, index: 0 };
8743
8744 pretty_assert_eq!(
8745 server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
8746 api::InscriptionRecursive {
8747 charms: vec![Charm::Burned],
8748 content_type: Some("text/html".into()),
8749 content_length: Some(3),
8750 delegate: None,
8751 fee: 0,
8752 height: 2,
8753 id,
8754 number: 0,
8755 output: OutPoint { txid, vout: 0 },
8756 sat: None,
8757 satpoint: SatPoint {
8758 outpoint: OutPoint { txid, vout: 0 },
8759 offset: 0
8760 },
8761 timestamp: 2,
8762 value: Some(50 * COIN_VALUE),
8763 address: None
8764 }
8765 );
8766 }
8767
8768 #[test]
8769 fn burned_charm_on_transfer() {
8770 let server = TestServer::builder().chain(Chain::Regtest).build();
8771
8772 server.mine_blocks(1);
8773
8774 let inscription = Inscription {
8775 content_type: Some("text/html".into()),
8776 body: Some("foo".into()),
8777 ..default()
8778 };
8779
8780 let create_txid = server.core.broadcast_tx(TransactionTemplate {
8781 inputs: &[(1, 0, 0, inscription.to_witness())],
8782 outputs: 1,
8783 ..default()
8784 });
8785
8786 server.mine_blocks(1);
8787
8788 let id = InscriptionId {
8789 txid: create_txid,
8790 index: 0,
8791 };
8792
8793 pretty_assert_eq!(
8794 server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
8795 api::InscriptionRecursive {
8796 charms: vec![],
8797 content_type: Some("text/html".into()),
8798 content_length: Some(3),
8799 delegate: None,
8800 fee: 0,
8801 height: 2,
8802 id,
8803 number: 0,
8804 output: OutPoint {
8805 txid: create_txid,
8806 vout: 0
8807 },
8808 sat: None,
8809 satpoint: SatPoint {
8810 outpoint: OutPoint {
8811 txid: create_txid,
8812 vout: 0
8813 },
8814 offset: 0
8815 },
8816 timestamp: 2,
8817 value: Some(50 * COIN_VALUE),
8818 address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
8819 }
8820 );
8821
8822 let transfer_txid = server.core.broadcast_tx(TransactionTemplate {
8823 inputs: &[(2, 1, 0, Default::default())],
8824 fee: 0,
8825 outputs: 0,
8826 op_return_index: Some(0),
8827 op_return_value: Some(50 * COIN_VALUE),
8828 op_return: Some(
8829 script::Builder::new()
8830 .push_opcode(opcodes::all::OP_RETURN)
8831 .into_script(),
8832 ),
8833 ..default()
8834 });
8835
8836 server.mine_blocks(1);
8837
8838 pretty_assert_eq!(
8839 server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
8840 api::InscriptionRecursive {
8841 charms: vec![Charm::Burned],
8842 content_type: Some("text/html".into()),
8843 content_length: Some(3),
8844 delegate: None,
8845 fee: 0,
8846 height: 2,
8847 id,
8848 number: 0,
8849 output: OutPoint {
8850 txid: transfer_txid,
8851 vout: 0
8852 },
8853 sat: None,
8854 satpoint: SatPoint {
8855 outpoint: OutPoint {
8856 txid: transfer_txid,
8857 vout: 0
8858 },
8859 offset: 0
8860 },
8861 timestamp: 2,
8862 value: Some(50 * COIN_VALUE),
8863 address: None
8864 }
8865 );
8866 }
8867
8868 #[test]
8869 fn unknown_output_returns_404() {
8870 let server = TestServer::builder().chain(Chain::Regtest).build();
8871 server.assert_response(
8872 "/output/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123",
8873 StatusCode::NOT_FOUND,
8874 "output 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123 not found",
8875 );
8876 }
8877
8878 #[test]
8879 fn satscard_form_with_coinkite_url_redirects_to_query() {
8880 TestServer::new().assert_redirect(
8881 &format!(
8882 "/satscard?url={}",
8883 urlencoding::encode(satscard::tests::COINKITE_URL)
8884 ),
8885 &format!("/satscard?{}", satscard::tests::coinkite_fragment()),
8886 );
8887 }
8888
8889 #[test]
8890 fn satscard_form_with_ordinals_url_redirects_to_query() {
8891 TestServer::new().assert_redirect(
8892 &format!(
8893 "/satscard?url={}",
8894 urlencoding::encode(satscard::tests::ORDINALS_URL)
8895 ),
8896 &format!("/satscard?{}", satscard::tests::ordinals_query()),
8897 );
8898 }
8899
8900 #[test]
8901 fn satscard_missing_form_query_is_error() {
8902 TestServer::new().assert_response(
8903 "/satscard?url=https://foo.com",
8904 StatusCode::BAD_REQUEST,
8905 "satscard URL missing fragment",
8906 );
8907 }
8908
8909 #[test]
8910 fn satscard_invalid_query_parameters() {
8911 TestServer::new().assert_response(
8912 "/satscard?foo=bar",
8913 StatusCode::BAD_REQUEST,
8914 "invalid satscard query parameters: unknown key `foo`",
8915 );
8916 }
8917
8918 #[test]
8919 fn satscard_empty_query_parameters_are_allowed() {
8920 TestServer::builder()
8921 .chain(Chain::Mainnet)
8922 .build()
8923 .assert_html("/satscard?", SatscardHtml { satscard: None });
8924 }
8925
8926 #[test]
8927 fn satscard_display_without_address_index() {
8928 TestServer::builder()
8929 .chain(Chain::Mainnet)
8930 .build()
8931 .assert_html(
8932 format!("/satscard?{}", satscard::tests::coinkite_fragment()),
8933 SatscardHtml {
8934 satscard: Some((satscard::tests::coinkite_satscard(), None)),
8935 },
8936 );
8937 }
8938
8939 #[test]
8940 fn satscard_coinkite_display_with_address_index_empty() {
8941 TestServer::builder()
8942 .chain(Chain::Mainnet)
8943 .index_addresses()
8944 .build()
8945 .assert_html(
8946 format!("/satscard?{}", satscard::tests::coinkite_fragment()),
8947 SatscardHtml {
8948 satscard: Some((
8949 satscard::tests::coinkite_satscard(),
8950 Some(AddressHtml {
8951 address: satscard::tests::coinkite_address(),
8952 header: false,
8953 inscriptions: Some(Vec::new()),
8954 outputs: Vec::new(),
8955 runes_balances: None,
8956 sat_balance: 0,
8957 }),
8958 )),
8959 },
8960 );
8961 }
8962
8963 #[test]
8964 fn satscard_ordinals_display_with_address_index_empty() {
8965 TestServer::builder()
8966 .chain(Chain::Mainnet)
8967 .index_addresses()
8968 .build()
8969 .assert_html(
8970 format!("/satscard?{}", satscard::tests::ordinals_query()),
8971 SatscardHtml {
8972 satscard: Some((
8973 satscard::tests::ordinals_satscard(),
8974 Some(AddressHtml {
8975 address: satscard::tests::ordinals_address(),
8976 header: false,
8977 inscriptions: Some(Vec::new()),
8978 outputs: Vec::new(),
8979 runes_balances: None,
8980 sat_balance: 0,
8981 }),
8982 )),
8983 },
8984 );
8985 }
8986
8987 #[test]
8988 fn satscard_address_recovery_fails_on_wrong_chain() {
8989 TestServer::builder()
8990 .chain(Chain::Testnet)
8991 .build()
8992 .assert_response(
8993 format!("/satscard?{}", satscard::tests::coinkite_fragment()),
8994 StatusCode::BAD_REQUEST,
8995 "invalid satscard query parameters: address recovery failed",
8996 );
8997 }
8998
8999 #[test]
9000 fn sat_inscription_at_index_content_endpoint() {
9001 let server = TestServer::builder()
9002 .index_sats()
9003 .chain(Chain::Regtest)
9004 .build();
9005
9006 server.mine_blocks(1);
9007
9008 let first_txid = server.core.broadcast_tx(TransactionTemplate {
9009 inputs: &[(
9010 1,
9011 0,
9012 0,
9013 inscription("text/plain;charset=utf-8", "foo").to_witness(),
9014 )],
9015 ..default()
9016 });
9017
9018 server.mine_blocks(1);
9019
9020 let first_inscription_id = InscriptionId {
9021 txid: first_txid,
9022 index: 0,
9023 };
9024
9025 let first_inscription = server
9026 .get_json::<api::InscriptionRecursive>(format!("/r/inscription/{first_inscription_id}"));
9027
9028 let sat = first_inscription.sat.unwrap();
9029
9030 server.assert_response(format!("/r/sat/{sat}/at/0/content"), StatusCode::OK, "foo");
9031
9032 server.assert_response(format!("/r/sat/{sat}/at/-1/content"), StatusCode::OK, "foo");
9033
9034 server.core.broadcast_tx(TransactionTemplate {
9035 inputs: &[(
9036 2,
9037 1,
9038 first_inscription.satpoint.outpoint.vout.try_into().unwrap(),
9039 inscription("text/plain;charset=utf-8", "bar").to_witness(),
9040 )],
9041 ..default()
9042 });
9043
9044 server.mine_blocks(1);
9045
9046 server.assert_response(format!("/r/sat/{sat}/at/0/content"), StatusCode::OK, "foo");
9047
9048 server.assert_response(format!("/r/sat/{sat}/at/1/content"), StatusCode::OK, "bar");
9049
9050 server.assert_response(format!("/r/sat/{sat}/at/-1/content"), StatusCode::OK, "bar");
9051
9052 server.assert_response(
9053 "/r/sat/0/at/0/content",
9054 StatusCode::NOT_FOUND,
9055 "inscription on sat 0 not found",
9056 );
9057
9058 let server = TestServer::new();
9059
9060 server.assert_response(
9061 "/r/sat/0/at/0/content",
9062 StatusCode::NOT_FOUND,
9063 "this server has no sat index",
9064 );
9065 }
9066
9067 #[test]
9068 fn offers_are_accepted() {
9069 let server = TestServer::builder().server_flag("--accept-offers").build();
9070
9071 let psbt0 = base64_encode(
9072 &Psbt {
9073 unsigned_tx: Transaction {
9074 version: Version(0),
9075 lock_time: LockTime::ZERO,
9076 input: Vec::new(),
9077 output: Vec::new(),
9078 },
9079 version: 0,
9080 xpub: BTreeMap::new(),
9081 proprietary: BTreeMap::new(),
9082 unknown: BTreeMap::new(),
9083 inputs: Vec::new(),
9084 outputs: Vec::new(),
9085 }
9086 .serialize(),
9087 );
9088
9089 let response = server.post("offer", &psbt0, StatusCode::OK);
9090
9091 assert_eq!(response.text().unwrap(), "");
9092
9093 let offers = server.get_json::<api::Offers>("/offers");
9094
9095 assert_eq!(
9096 offers,
9097 api::Offers {
9098 offers: vec![psbt0.clone()],
9099 },
9100 );
9101
9102 let psbt1 = base64_encode(
9103 &Psbt {
9104 unsigned_tx: Transaction {
9105 version: Version(1),
9106 lock_time: LockTime::ZERO,
9107 input: Vec::new(),
9108 output: Vec::new(),
9109 },
9110 version: 0,
9111 xpub: BTreeMap::new(),
9112 proprietary: BTreeMap::new(),
9113 unknown: BTreeMap::new(),
9114 inputs: Vec::new(),
9115 outputs: Vec::new(),
9116 }
9117 .serialize(),
9118 );
9119
9120 let response = server.post("offer", &psbt1, StatusCode::OK);
9121
9122 assert_eq!(response.text().unwrap(), "");
9123
9124 let offers = server.get_json::<api::Offers>("/offers");
9125
9126 assert_eq!(
9127 offers,
9128 api::Offers {
9129 offers: vec![psbt0, psbt1],
9130 },
9131 );
9132 }
9133
9134 #[test]
9135 fn offers_are_rejected_if_not_valid_psbts() {
9136 let server = TestServer::builder().server_flag("--accept-offers").build();
9137 server.post("offer", "0", StatusCode::BAD_REQUEST);
9138 }
9139
9140 #[test]
9141 fn offer_acceptance_requires_accept_offers_flag() {
9142 let server = TestServer::builder().build();
9143
9144 let psbt = base64_encode(
9145 &Psbt {
9146 unsigned_tx: Transaction {
9147 version: Version(0),
9148 lock_time: LockTime::ZERO,
9149 input: Vec::new(),
9150 output: Vec::new(),
9151 },
9152 version: 0,
9153 xpub: BTreeMap::new(),
9154 proprietary: BTreeMap::new(),
9155 unknown: BTreeMap::new(),
9156 inputs: Vec::new(),
9157 outputs: Vec::new(),
9158 }
9159 .serialize(),
9160 );
9161
9162 server.post("offer", &psbt, StatusCode::NOT_FOUND);
9163 }
9164
9165 #[test]
9166 fn offer_acceptance_does_not_require_json_api() {
9167 let server = TestServer::builder()
9168 .server_flag("--disable-json-api")
9169 .server_flag("--accept-offers")
9170 .build();
9171
9172 let psbt = base64_encode(
9173 &Psbt {
9174 unsigned_tx: Transaction {
9175 version: Version(0),
9176 lock_time: LockTime::ZERO,
9177 input: Vec::new(),
9178 output: Vec::new(),
9179 },
9180 version: 0,
9181 xpub: BTreeMap::new(),
9182 proprietary: BTreeMap::new(),
9183 unknown: BTreeMap::new(),
9184 inputs: Vec::new(),
9185 outputs: Vec::new(),
9186 }
9187 .serialize(),
9188 );
9189
9190 server.post("offer", &psbt, StatusCode::OK);
9191 }
9192
9193 #[test]
9194 fn offer_size_is_limited() {
9195 let server = TestServer::builder().server_flag("--accept-offers").build();
9196
9197 let psbt = base64_encode(
9198 &Psbt {
9199 unsigned_tx: Transaction {
9200 version: Version(0),
9201 lock_time: LockTime::ZERO,
9202 input: Vec::new(),
9203 output: vec![TxOut {
9204 value: Amount::from_sat(1),
9205 script_pubkey: ScriptBuf::builder()
9206 .push_slice::<&PushBytes>(vec![0; 2 * MEBIBYTE].as_slice().try_into().unwrap())
9207 .into_script(),
9208 }],
9209 },
9210 version: 0,
9211 xpub: BTreeMap::new(),
9212 proprietary: BTreeMap::new(),
9213 unknown: BTreeMap::new(),
9214 inputs: Vec::new(),
9215 outputs: vec![bitcoin::psbt::Output::default()],
9216 }
9217 .serialize(),
9218 );
9219
9220 server.post("offer", &psbt, StatusCode::PAYLOAD_TOO_LARGE);
9221 }
9222
9223 #[test]
9224 fn missing_returns_missing_inscription_ids() {
9225 let server = TestServer::builder().chain(Chain::Regtest).build();
9226
9227 server.mine_blocks(1);
9228
9229 let txid = server.core.broadcast_tx(TransactionTemplate {
9230 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
9231 ..default()
9232 });
9233
9234 server.mine_blocks(1);
9235
9236 let existing = InscriptionId { txid, index: 0 };
9237
9238 let missing_id = "0000000000000000000000000000000000000000000000000000000000000000i0"
9239 .parse::<InscriptionId>()
9240 .unwrap();
9241
9242 let result = server.post_json::<Vec<InscriptionId>>("missing", &vec![existing, missing_id]);
9243
9244 assert_eq!(result, vec![missing_id]);
9245 }
9246
9247 #[test]
9248 fn missing_returns_empty_when_all_exist() {
9249 let server = TestServer::builder().chain(Chain::Regtest).build();
9250
9251 server.mine_blocks(1);
9252
9253 let txid = server.core.broadcast_tx(TransactionTemplate {
9254 inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
9255 ..default()
9256 });
9257
9258 server.mine_blocks(1);
9259
9260 let existing = InscriptionId { txid, index: 0 };
9261
9262 let result = server.post_json::<Vec<InscriptionId>>("missing", &vec![existing]);
9263
9264 assert!(result.is_empty());
9265 }
9266
9267 #[test]
9268 fn post_bodies_are_limited_when_json_api_is_disabled() {
9269 let server = TestServer::builder()
9270 .server_flag("--disable-json-api")
9271 .build();
9272
9273 let client = reqwest::blocking::Client::new();
9274
9275 let response = client
9276 .post(server.join_url("outputs"))
9277 .header(header::CONTENT_TYPE, "application/json")
9278 .body(" ".repeat(2 * MEBIBYTE + 1))
9279 .send()
9280 .unwrap();
9281
9282 assert_eq!(
9283 response.status(),
9284 StatusCode::PAYLOAD_TOO_LARGE,
9285 "{}",
9286 response.text().unwrap(),
9287 );
9288
9289 let response = client
9290 .post(server.join_url("inscriptions"))
9291 .header(header::CONTENT_TYPE, "application/json")
9292 .body(" ".repeat(2 * MEBIBYTE + 1))
9293 .send()
9294 .unwrap();
9295
9296 assert_eq!(
9297 response.status(),
9298 StatusCode::PAYLOAD_TOO_LARGE,
9299 "{}",
9300 response.text().unwrap(),
9301 );
9302 }
9303}