ord/subcommand/
server.rs

1use {
2  self::{
3    accept_encoding::AcceptEncoding,
4    accept_json::AcceptJson,
5    error::{OptionExt, ServerError, ServerResult},
6  },
7  super::*,
8  crate::templates::{
9    BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, HomeHtml, InputHtml,
10    InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent, PageHtml,
11    ParentsHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml,
12    PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml,
13    PreviewVideoHtml, RangeHtml, RareTxt, RuneHtml, RunesHtml, SatHtml, TransactionHtml,
14  },
15  axum::{
16    body,
17    extract::{DefaultBodyLimit, Extension, Json, Path, Query},
18    http::{header, HeaderValue, StatusCode, Uri},
19    response::{IntoResponse, Redirect, Response},
20    routing::{get, post},
21    Router,
22  },
23  axum_server::Handle,
24  brotli::Decompressor,
25  rust_embed::RustEmbed,
26  rustls_acme::{
27    acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY},
28    axum::AxumAcceptor,
29    caches::DirCache,
30    AcmeConfig,
31  },
32  std::{cmp::Ordering, str, sync::Arc},
33  tokio_stream::StreamExt,
34  tower_http::{
35    compression::CompressionLayer,
36    cors::{Any, CorsLayer},
37    set_header::SetResponseHeaderLayer,
38    validate_request::ValidateRequestHeaderLayer,
39  },
40};
41
42pub(crate) use server_config::ServerConfig;
43
44mod accept_encoding;
45mod accept_json;
46mod error;
47pub mod query;
48mod server_config;
49
50enum SpawnConfig {
51  Https(AxumAcceptor),
52  Http,
53  Redirect(String),
54}
55
56#[derive(Deserialize)]
57struct Search {
58  query: String,
59}
60
61#[derive(RustEmbed)]
62#[folder = "static"]
63struct StaticAssets;
64
65struct StaticHtml {
66  title: &'static str,
67  html: &'static str,
68}
69
70impl PageContent for StaticHtml {
71  fn title(&self) -> String {
72    self.title.into()
73  }
74}
75
76impl Display for StaticHtml {
77  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
78    f.write_str(self.html)
79  }
80}
81
82#[derive(Debug, Parser, Clone)]
83pub struct Server {
84  #[arg(
85    long,
86    help = "Listen on <ADDRESS> for incoming requests. [default: 0.0.0.0]"
87  )]
88  pub address: Option<String>,
89  #[arg(
90    long,
91    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."
92  )]
93  pub acme_domain: Vec<String>,
94  #[arg(
95    long,
96    help = "Use <CSP_ORIGIN> in Content-Security-Policy header. Set this to the public-facing URL of your ord instance."
97  )]
98  pub csp_origin: Option<String>,
99  #[arg(
100    long,
101    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."
102  )]
103  pub decompress: bool,
104  #[arg(long, help = "Disable JSON API.")]
105  pub disable_json_api: bool,
106  #[arg(
107    long,
108    help = "Listen on <HTTP_PORT> for incoming HTTP requests. [default: 80]"
109  )]
110  pub http_port: Option<u16>,
111  #[arg(
112    long,
113    group = "port",
114    help = "Listen on <HTTPS_PORT> for incoming HTTPS requests. [default: 443]"
115  )]
116  pub https_port: Option<u16>,
117  #[arg(long, help = "Store ACME TLS certificates in <ACME_CACHE>.")]
118  pub acme_cache: Option<PathBuf>,
119  #[arg(long, help = "Provide ACME contact <ACME_CONTACT>.")]
120  pub acme_contact: Vec<String>,
121  #[arg(long, help = "Serve HTTP traffic on <HTTP_PORT>.")]
122  pub http: bool,
123  #[arg(long, help = "Serve HTTPS traffic on <HTTPS_PORT>.")]
124  pub https: bool,
125  #[arg(long, help = "Redirect HTTP traffic to HTTPS.")]
126  pub redirect_http_to_https: bool,
127  #[arg(long, alias = "nosync", help = "Do not update the index.")]
128  pub no_sync: bool,
129  #[arg(
130    long,
131    help = "Proxy `/content/INSCRIPTION_ID` requests to `<CONTENT_PROXY>/content/INSCRIPTION_ID` if the inscription is not present on current chain."
132  )]
133  pub content_proxy: Option<Url>,
134  #[arg(
135    long,
136    default_value = "5s",
137    help = "Poll Bitcoin Core every <POLLING_INTERVAL>."
138  )]
139  pub polling_interval: humantime::Duration,
140}
141
142impl Server {
143  pub fn run(self, settings: Settings, index: Arc<Index>, handle: Handle) -> SubcommandResult {
144    Runtime::new()?.block_on(async {
145      let settings = Arc::new(settings);
146      let acme_domains = self.acme_domains()?;
147
148      let server_config = Arc::new(ServerConfig {
149        chain: settings.chain(),
150        content_proxy: self.content_proxy.clone(),
151        csp_origin: self.csp_origin.clone(),
152        decompress: self.decompress,
153        domain: acme_domains.first().cloned(),
154        index_sats: index.has_sat_index(),
155        json_api_enabled: !self.disable_json_api,
156      });
157
158      let router = Router::new()
159        .route("/", get(Self::home))
160        .route("/block/:query", get(Self::block))
161        .route("/blockcount", get(Self::block_count))
162        .route("/blockhash", get(Self::block_hash))
163        .route("/blockhash/:height", get(Self::block_hash_from_height))
164        .route("/blockheight", get(Self::block_height))
165        .route("/blocks", get(Self::blocks))
166        .route("/blocktime", get(Self::block_time))
167        .route("/bounties", get(Self::bounties))
168        .route("/children/:inscription_id", get(Self::children))
169        .route(
170          "/children/:inscription_id/:page",
171          get(Self::children_paginated),
172        )
173        .route("/clock", get(Self::clock))
174        .route("/collections", get(Self::collections))
175        .route("/collections/:page", get(Self::collections_paginated))
176        .route("/content/:inscription_id", get(Self::content))
177        .route("/faq", get(Self::faq))
178        .route("/favicon.ico", get(Self::favicon))
179        .route("/feed.xml", get(Self::feed))
180        .route("/input/:block/:transaction/:input", get(Self::input))
181        .route("/inscription/:inscription_query", get(Self::inscription))
182        .route("/inscriptions", get(Self::inscriptions))
183        .route("/inscriptions", post(Self::inscriptions_json))
184        .route("/inscriptions/:page", get(Self::inscriptions_paginated))
185        .route(
186          "/inscriptions/block/:height",
187          get(Self::inscriptions_in_block),
188        )
189        .route(
190          "/inscriptions/block/:height/:page",
191          get(Self::inscriptions_in_block_paginated),
192        )
193        .route("/install.sh", get(Self::install_script))
194        .route("/ordinal/:sat", get(Self::ordinal))
195        .route("/output/:output", get(Self::output))
196        .route("/outputs", post(Self::outputs))
197        .route("/parents/:inscription_id", get(Self::parents))
198        .route(
199          "/parents/:inscription_id/:page",
200          get(Self::parents_paginated),
201        )
202        .route("/preview/:inscription_id", get(Self::preview))
203        .route("/r/blockhash", get(Self::block_hash_json))
204        .route(
205          "/r/blockhash/:height",
206          get(Self::block_hash_from_height_json),
207        )
208        .route("/r/blockheight", get(Self::block_height))
209        .route("/r/blocktime", get(Self::block_time))
210        .route("/r/blockinfo/:query", get(Self::block_info))
211        .route(
212          "/r/inscription/:inscription_id",
213          get(Self::inscription_recursive),
214        )
215        .route("/r/children/:inscription_id", get(Self::children_recursive))
216        .route(
217          "/r/children/:inscription_id/:page",
218          get(Self::children_recursive_paginated),
219        )
220        .route("/r/metadata/:inscription_id", get(Self::metadata))
221        .route("/r/sat/:sat_number", get(Self::sat_inscriptions))
222        .route(
223          "/r/sat/:sat_number/:page",
224          get(Self::sat_inscriptions_paginated),
225        )
226        .route(
227          "/r/sat/:sat_number/at/:index",
228          get(Self::sat_inscription_at_index),
229        )
230        .route("/range/:start/:end", get(Self::range))
231        .route("/rare.txt", get(Self::rare_txt))
232        .route("/rune/:rune", get(Self::rune))
233        .route("/runes", get(Self::runes))
234        .route("/runes/:page", get(Self::runes_paginated))
235        .route("/runes/balances", get(Self::runes_balances))
236        .route("/sat/:sat", get(Self::sat))
237        .route("/search", get(Self::search_by_query))
238        .route("/search/*query", get(Self::search_by_path))
239        .route("/static/*path", get(Self::static_asset))
240        .route("/status", get(Self::status))
241        .route("/tx/:txid", get(Self::transaction))
242        .route("/update", get(Self::update))
243        .fallback(Self::fallback)
244        .layer(Extension(index))
245        .layer(Extension(server_config.clone()))
246        .layer(Extension(settings.clone()))
247        .layer(SetResponseHeaderLayer::if_not_present(
248          header::CONTENT_SECURITY_POLICY,
249          HeaderValue::from_static("default-src 'self'"),
250        ))
251        .layer(SetResponseHeaderLayer::overriding(
252          header::STRICT_TRANSPORT_SECURITY,
253          HeaderValue::from_static("max-age=31536000; includeSubDomains; preload"),
254        ))
255        .layer(
256          CorsLayer::new()
257            .allow_methods([http::Method::GET])
258            .allow_origin(Any),
259        )
260        .layer(CompressionLayer::new())
261        .with_state(server_config.clone());
262
263      let router = if server_config.json_api_enabled {
264        router.layer(DefaultBodyLimit::disable())
265      } else {
266        router
267      };
268
269      let router = if let Some((username, password)) = settings.credentials() {
270        router.layer(ValidateRequestHeaderLayer::basic(username, password))
271      } else {
272        router
273      };
274
275      match (self.http_port(), self.https_port()) {
276        (Some(http_port), None) => {
277          self
278            .spawn(&settings, router, handle, http_port, SpawnConfig::Http)?
279            .await??
280        }
281        (None, Some(https_port)) => {
282          self
283            .spawn(
284              &settings,
285              router,
286              handle,
287              https_port,
288              SpawnConfig::Https(self.acceptor(&settings)?),
289            )?
290            .await??
291        }
292        (Some(http_port), Some(https_port)) => {
293          let http_spawn_config = if self.redirect_http_to_https {
294            SpawnConfig::Redirect(if https_port == 443 {
295              format!("https://{}", acme_domains[0])
296            } else {
297              format!("https://{}:{https_port}", acme_domains[0])
298            })
299          } else {
300            SpawnConfig::Http
301          };
302
303          let (http_result, https_result) = tokio::join!(
304            self.spawn(
305              &settings,
306              router.clone(),
307              handle.clone(),
308              http_port,
309              http_spawn_config
310            )?,
311            self.spawn(
312              &settings,
313              router,
314              handle,
315              https_port,
316              SpawnConfig::Https(self.acceptor(&settings)?),
317            )?
318          );
319          http_result.and(https_result)??;
320        }
321        (None, None) => unreachable!(),
322      }
323
324      Ok(None)
325    })
326  }
327
328  fn spawn(
329    &self,
330    settings: &Settings,
331    router: Router,
332    handle: Handle,
333    port: u16,
334    config: SpawnConfig,
335  ) -> Result<task::JoinHandle<io::Result<()>>> {
336    let address = match &self.address {
337      Some(address) => address.as_str(),
338      None => {
339        if cfg!(test) || settings.integration_test() {
340          "127.0.0.1"
341        } else {
342          "0.0.0.0"
343        }
344      }
345    };
346
347    let addr = (address, port)
348      .to_socket_addrs()?
349      .next()
350      .ok_or_else(|| anyhow!("failed to get socket addrs"))?;
351
352    if !settings.integration_test() && !cfg!(test) {
353      eprintln!(
354        "Listening on {}://{addr}",
355        match config {
356          SpawnConfig::Https(_) => "https",
357          _ => "http",
358        }
359      );
360    }
361
362    Ok(tokio::spawn(async move {
363      match config {
364        SpawnConfig::Https(acceptor) => {
365          axum_server::Server::bind(addr)
366            .handle(handle)
367            .acceptor(acceptor)
368            .serve(router.into_make_service())
369            .await
370        }
371        SpawnConfig::Redirect(destination) => {
372          axum_server::Server::bind(addr)
373            .handle(handle)
374            .serve(
375              Router::new()
376                .fallback(Self::redirect_http_to_https)
377                .layer(Extension(destination))
378                .into_make_service(),
379            )
380            .await
381        }
382        SpawnConfig::Http => {
383          axum_server::Server::bind(addr)
384            .handle(handle)
385            .serve(router.into_make_service())
386            .await
387        }
388      }
389    }))
390  }
391
392  fn acme_cache(acme_cache: Option<&PathBuf>, settings: &Settings) -> PathBuf {
393    match acme_cache {
394      Some(acme_cache) => acme_cache.clone(),
395      None => settings.data_dir().join("acme-cache"),
396    }
397  }
398
399  fn acme_domains(&self) -> Result<Vec<String>> {
400    if !self.acme_domain.is_empty() {
401      Ok(self.acme_domain.clone())
402    } else {
403      Ok(vec![
404        System::host_name().ok_or(anyhow!("no hostname found"))?
405      ])
406    }
407  }
408
409  fn http_port(&self) -> Option<u16> {
410    if self.http || self.http_port.is_some() || (self.https_port.is_none() && !self.https) {
411      Some(self.http_port.unwrap_or(80))
412    } else {
413      None
414    }
415  }
416
417  fn https_port(&self) -> Option<u16> {
418    if self.https || self.https_port.is_some() {
419      Some(self.https_port.unwrap_or(443))
420    } else {
421      None
422    }
423  }
424
425  fn acceptor(&self, settings: &Settings) -> Result<AxumAcceptor> {
426    let config = AcmeConfig::new(self.acme_domains()?)
427      .contact(&self.acme_contact)
428      .cache_option(Some(DirCache::new(Self::acme_cache(
429        self.acme_cache.as_ref(),
430        settings,
431      ))))
432      .directory(if cfg!(test) {
433        LETS_ENCRYPT_STAGING_DIRECTORY
434      } else {
435        LETS_ENCRYPT_PRODUCTION_DIRECTORY
436      });
437
438    let mut state = config.state();
439
440    let mut server_config = rustls::ServerConfig::builder()
441      .with_no_client_auth()
442      .with_cert_resolver(state.resolver());
443
444    server_config.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
445
446    let acceptor = state.axum_acceptor(Arc::new(server_config));
447
448    tokio::spawn(async move {
449      while let Some(result) = state.next().await {
450        match result {
451          Ok(ok) => log::info!("ACME event: {:?}", ok),
452          Err(err) => log::error!("ACME error: {:?}", err),
453        }
454      }
455    });
456
457    Ok(acceptor)
458  }
459
460  fn index_height(index: &Index) -> ServerResult<Height> {
461    index.block_height()?.ok_or_not_found(|| "genesis block")
462  }
463
464  async fn clock(Extension(index): Extension<Arc<Index>>) -> ServerResult {
465    task::block_in_place(|| {
466      Ok(
467        (
468          [(
469            header::CONTENT_SECURITY_POLICY,
470            HeaderValue::from_static("default-src 'unsafe-inline'"),
471          )],
472          ClockSvg::new(Self::index_height(&index)?),
473        )
474          .into_response(),
475      )
476    })
477  }
478
479  async fn fallback(Extension(index): Extension<Arc<Index>>, uri: Uri) -> ServerResult<Response> {
480    task::block_in_place(|| {
481      let path = urlencoding::decode(uri.path().trim_matches('/'))
482        .map_err(|err| ServerError::BadRequest(err.to_string()))?;
483
484      let prefix = if re::INSCRIPTION_ID.is_match(&path) || re::INSCRIPTION_NUMBER.is_match(&path) {
485        "inscription"
486      } else if re::RUNE_ID.is_match(&path) || re::SPACED_RUNE.is_match(&path) {
487        "rune"
488      } else if re::OUTPOINT.is_match(&path) {
489        "output"
490      } else if re::HASH.is_match(&path) {
491        if index.block_header(path.parse().unwrap())?.is_some() {
492          "block"
493        } else {
494          "tx"
495        }
496      } else {
497        return Ok(StatusCode::NOT_FOUND.into_response());
498      };
499
500      Ok(Redirect::to(&format!("/{prefix}/{path}")).into_response())
501    })
502  }
503
504  async fn sat(
505    Extension(server_config): Extension<Arc<ServerConfig>>,
506    Extension(index): Extension<Arc<Index>>,
507    Path(DeserializeFromStr(sat)): Path<DeserializeFromStr<Sat>>,
508    AcceptJson(accept_json): AcceptJson,
509  ) -> ServerResult {
510    task::block_in_place(|| {
511      let inscriptions = index.get_inscription_ids_by_sat(sat)?;
512      let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| {
513        inscriptions.first().and_then(|&first_inscription_id| {
514          index
515            .get_inscription_satpoint_by_id(first_inscription_id)
516            .ok()
517            .flatten()
518        })
519      });
520      let blocktime = index.block_time(sat.height())?;
521
522      let charms = sat.charms();
523
524      Ok(if accept_json {
525        Json(api::Sat {
526          number: sat.0,
527          decimal: sat.decimal().to_string(),
528          degree: sat.degree().to_string(),
529          name: sat.name(),
530          block: sat.height().0,
531          cycle: sat.cycle(),
532          epoch: sat.epoch().0,
533          period: sat.period(),
534          offset: sat.third(),
535          rarity: sat.rarity(),
536          percentile: sat.percentile(),
537          satpoint,
538          timestamp: blocktime.timestamp().timestamp(),
539          inscriptions,
540          charms: Charm::charms(charms),
541        })
542        .into_response()
543      } else {
544        SatHtml {
545          sat,
546          satpoint,
547          blocktime,
548          inscriptions,
549        }
550        .page(server_config)
551        .into_response()
552      })
553    })
554  }
555
556  async fn ordinal(Path(sat): Path<String>) -> Redirect {
557    Redirect::to(&format!("/sat/{sat}"))
558  }
559
560  async fn output(
561    Extension(server_config): Extension<Arc<ServerConfig>>,
562    Extension(index): Extension<Arc<Index>>,
563    Path(outpoint): Path<OutPoint>,
564    AcceptJson(accept_json): AcceptJson,
565  ) -> ServerResult {
566    task::block_in_place(|| {
567      let (output_info, txout) = index
568        .get_output_info(outpoint)?
569        .ok_or_not_found(|| format!("output {outpoint}"))?;
570
571      Ok(if accept_json {
572        Json(output_info).into_response()
573      } else {
574        OutputHtml {
575          chain: server_config.chain,
576          inscriptions: output_info.inscriptions,
577          outpoint,
578          output: txout,
579          runes: output_info.runes,
580          sat_ranges: output_info.sat_ranges,
581          spent: output_info.spent,
582        }
583        .page(server_config)
584        .into_response()
585      })
586    })
587  }
588
589  async fn outputs(
590    Extension(index): Extension<Arc<Index>>,
591    AcceptJson(accept_json): AcceptJson,
592    Json(outputs): Json<Vec<OutPoint>>,
593  ) -> ServerResult {
594    task::block_in_place(|| {
595      Ok(if accept_json {
596        let mut response = Vec::new();
597        for outpoint in outputs {
598          let (output_info, _) = index
599            .get_output_info(outpoint)?
600            .ok_or_not_found(|| format!("output {outpoint}"))?;
601
602          response.push(output_info);
603        }
604        Json(response).into_response()
605      } else {
606        StatusCode::NOT_FOUND.into_response()
607      })
608    })
609  }
610
611  async fn range(
612    Extension(server_config): Extension<Arc<ServerConfig>>,
613    Path((DeserializeFromStr(start), DeserializeFromStr(end))): Path<(
614      DeserializeFromStr<Sat>,
615      DeserializeFromStr<Sat>,
616    )>,
617  ) -> ServerResult<PageHtml<RangeHtml>> {
618    match start.cmp(&end) {
619      Ordering::Equal => Err(ServerError::BadRequest("empty range".to_string())),
620      Ordering::Greater => Err(ServerError::BadRequest(
621        "range start greater than range end".to_string(),
622      )),
623      Ordering::Less => Ok(RangeHtml { start, end }.page(server_config)),
624    }
625  }
626
627  async fn rare_txt(Extension(index): Extension<Arc<Index>>) -> ServerResult<RareTxt> {
628    task::block_in_place(|| Ok(RareTxt(index.rare_sat_satpoints()?)))
629  }
630
631  async fn rune(
632    Extension(server_config): Extension<Arc<ServerConfig>>,
633    Extension(index): Extension<Arc<Index>>,
634    Path(DeserializeFromStr(rune_query)): Path<DeserializeFromStr<query::Rune>>,
635    AcceptJson(accept_json): AcceptJson,
636  ) -> ServerResult {
637    task::block_in_place(|| {
638      if !index.has_rune_index() {
639        return Err(ServerError::NotFound(
640          "this server has no rune index".to_string(),
641        ));
642      }
643
644      let rune = match rune_query {
645        query::Rune::Spaced(spaced_rune) => spaced_rune.rune,
646        query::Rune::Id(rune_id) => index
647          .get_rune_by_id(rune_id)?
648          .ok_or_not_found(|| format!("rune {rune_id}"))?,
649        query::Rune::Number(number) => index
650          .get_rune_by_number(usize::try_from(number).unwrap())?
651          .ok_or_not_found(|| format!("rune number {number}"))?,
652      };
653
654      let (id, entry, parent) = index
655        .rune(rune)?
656        .ok_or_not_found(|| format!("rune {rune}"))?;
657
658      let block_height = index.block_height()?.unwrap_or(Height(0));
659
660      let mintable = entry.mintable((block_height.n() + 1).into()).is_ok();
661
662      Ok(if accept_json {
663        Json(api::Rune {
664          entry,
665          id,
666          mintable,
667          parent,
668        })
669        .into_response()
670      } else {
671        RuneHtml {
672          entry,
673          id,
674          mintable,
675          parent,
676        }
677        .page(server_config)
678        .into_response()
679      })
680    })
681  }
682
683  async fn runes(
684    Extension(server_config): Extension<Arc<ServerConfig>>,
685    Extension(index): Extension<Arc<Index>>,
686    accept_json: AcceptJson,
687  ) -> ServerResult<Response> {
688    Self::runes_paginated(
689      Extension(server_config),
690      Extension(index),
691      Path(0),
692      accept_json,
693    )
694    .await
695  }
696
697  async fn runes_paginated(
698    Extension(server_config): Extension<Arc<ServerConfig>>,
699    Extension(index): Extension<Arc<Index>>,
700    Path(page_index): Path<usize>,
701    AcceptJson(accept_json): AcceptJson,
702  ) -> ServerResult {
703    task::block_in_place(|| {
704      let (entries, more) = index.runes_paginated(50, page_index)?;
705
706      let prev = page_index.checked_sub(1);
707
708      let next = more.then_some(page_index + 1);
709
710      Ok(if accept_json {
711        Json(RunesHtml {
712          entries,
713          more,
714          prev,
715          next,
716        })
717        .into_response()
718      } else {
719        RunesHtml {
720          entries,
721          more,
722          prev,
723          next,
724        }
725        .page(server_config)
726        .into_response()
727      })
728    })
729  }
730
731  async fn runes_balances(
732    Extension(index): Extension<Arc<Index>>,
733    AcceptJson(accept_json): AcceptJson,
734  ) -> ServerResult {
735    task::block_in_place(|| {
736      Ok(if accept_json {
737        Json(
738          index
739            .get_rune_balance_map()?
740            .into_iter()
741            .map(|(rune, balances)| {
742              (
743                rune,
744                balances
745                  .into_iter()
746                  .map(|(outpoint, pile)| (outpoint, pile.amount))
747                  .collect(),
748              )
749            })
750            .collect::<BTreeMap<SpacedRune, BTreeMap<OutPoint, u128>>>(),
751        )
752        .into_response()
753      } else {
754        StatusCode::NOT_FOUND.into_response()
755      })
756    })
757  }
758
759  async fn home(
760    Extension(server_config): Extension<Arc<ServerConfig>>,
761    Extension(index): Extension<Arc<Index>>,
762  ) -> ServerResult<PageHtml<HomeHtml>> {
763    task::block_in_place(|| {
764      Ok(
765        HomeHtml {
766          inscriptions: index.get_home_inscriptions()?,
767        }
768        .page(server_config),
769      )
770    })
771  }
772
773  async fn blocks(
774    Extension(server_config): Extension<Arc<ServerConfig>>,
775    Extension(index): Extension<Arc<Index>>,
776    AcceptJson(accept_json): AcceptJson,
777  ) -> ServerResult {
778    task::block_in_place(|| {
779      let blocks = index.blocks(100)?;
780      let mut featured_blocks = BTreeMap::new();
781      for (height, hash) in blocks.iter().take(5) {
782        let (inscriptions, _total_num) =
783          index.get_highest_paying_inscriptions_in_block(*height, 8)?;
784
785        featured_blocks.insert(*hash, inscriptions);
786      }
787
788      Ok(if accept_json {
789        Json(api::Blocks::new(blocks, featured_blocks)).into_response()
790      } else {
791        BlocksHtml::new(blocks, featured_blocks)
792          .page(server_config)
793          .into_response()
794      })
795    })
796  }
797
798  async fn install_script() -> Redirect {
799    Redirect::to("https://raw.githubusercontent.com/ordinals/ord/master/install.sh")
800  }
801
802  async fn block(
803    Extension(server_config): Extension<Arc<ServerConfig>>,
804    Extension(index): Extension<Arc<Index>>,
805    Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Block>>,
806    AcceptJson(accept_json): AcceptJson,
807  ) -> ServerResult {
808    task::block_in_place(|| {
809      let (block, height) = match query {
810        query::Block::Height(height) => {
811          let block = index
812            .get_block_by_height(height)?
813            .ok_or_not_found(|| format!("block {height}"))?;
814
815          (block, height)
816        }
817        query::Block::Hash(hash) => {
818          let info = index
819            .block_header_info(hash)?
820            .ok_or_not_found(|| format!("block {hash}"))?;
821
822          let block = index
823            .get_block_by_hash(hash)?
824            .ok_or_not_found(|| format!("block {hash}"))?;
825
826          (block, u32::try_from(info.height).unwrap())
827        }
828      };
829
830      let runes = index.get_runes_in_block(u64::from(height))?;
831      Ok(if accept_json {
832        let inscriptions = index.get_inscriptions_in_block(height)?;
833        Json(api::Block::new(
834          block,
835          Height(height),
836          Self::index_height(&index)?,
837          inscriptions,
838          runes,
839        ))
840        .into_response()
841      } else {
842        let (featured_inscriptions, total_num) =
843          index.get_highest_paying_inscriptions_in_block(height, 8)?;
844        BlockHtml::new(
845          block,
846          Height(height),
847          Self::index_height(&index)?,
848          total_num,
849          featured_inscriptions,
850          runes,
851        )
852        .page(server_config)
853        .into_response()
854      })
855    })
856  }
857
858  async fn transaction(
859    Extension(server_config): Extension<Arc<ServerConfig>>,
860    Extension(index): Extension<Arc<Index>>,
861    Path(txid): Path<Txid>,
862    AcceptJson(accept_json): AcceptJson,
863  ) -> ServerResult {
864    task::block_in_place(|| {
865      let transaction = index
866        .get_transaction(txid)?
867        .ok_or_not_found(|| format!("transaction {txid}"))?;
868
869      let inscription_count = index.inscription_count(txid)?;
870
871      Ok(if accept_json {
872        Json(api::Transaction {
873          chain: server_config.chain,
874          etching: index.get_etching(txid)?,
875          inscription_count,
876          transaction,
877          txid,
878        })
879        .into_response()
880      } else {
881        TransactionHtml {
882          chain: server_config.chain,
883          etching: index.get_etching(txid)?,
884          inscription_count,
885          transaction,
886          txid,
887        }
888        .page(server_config)
889        .into_response()
890      })
891    })
892  }
893
894  async fn update(
895    Extension(settings): Extension<Arc<Settings>>,
896    Extension(index): Extension<Arc<Index>>,
897  ) -> ServerResult {
898    task::block_in_place(|| {
899      if settings.integration_test() {
900        index.update()?;
901        Ok(index.block_count()?.to_string().into_response())
902      } else {
903        Ok(StatusCode::NOT_FOUND.into_response())
904      }
905    })
906  }
907
908  async fn metadata(
909    Extension(index): Extension<Arc<Index>>,
910    Path(inscription_id): Path<InscriptionId>,
911  ) -> ServerResult<Json<String>> {
912    task::block_in_place(|| {
913      let metadata = index
914        .get_inscription_by_id(inscription_id)?
915        .ok_or_not_found(|| format!("inscription {inscription_id}"))?
916        .metadata
917        .ok_or_not_found(|| format!("inscription {inscription_id} metadata"))?;
918
919      Ok(Json(hex::encode(metadata)))
920    })
921  }
922
923  async fn inscription_recursive(
924    Extension(index): Extension<Arc<Index>>,
925    Path(inscription_id): Path<InscriptionId>,
926  ) -> ServerResult {
927    task::block_in_place(|| {
928      let inscription = index
929        .get_inscription_by_id(inscription_id)?
930        .ok_or_not_found(|| format!("inscription {inscription_id}"))?;
931
932      let entry = index
933        .get_inscription_entry(inscription_id)
934        .unwrap()
935        .unwrap();
936
937      let satpoint = index
938        .get_inscription_satpoint_by_id(inscription_id)
939        .ok()
940        .flatten()
941        .unwrap();
942
943      let output = if satpoint.outpoint == unbound_outpoint() {
944        None
945      } else {
946        Some(
947          index
948            .get_transaction(satpoint.outpoint.txid)?
949            .ok_or_not_found(|| format!("inscription {inscription_id} current transaction"))?
950            .output
951            .into_iter()
952            .nth(satpoint.outpoint.vout.try_into().unwrap())
953            .ok_or_not_found(|| {
954              format!("inscription {inscription_id} current transaction output")
955            })?,
956        )
957      };
958
959      Ok(
960        Json(api::InscriptionRecursive {
961          charms: Charm::charms(entry.charms),
962          content_type: inscription.content_type().map(|s| s.to_string()),
963          content_length: inscription.content_length(),
964          fee: entry.fee,
965          height: entry.height,
966          id: inscription_id,
967          number: entry.inscription_number,
968          output: satpoint.outpoint,
969          value: output.as_ref().map(|o| o.value),
970          sat: entry.sat,
971          satpoint,
972          timestamp: timestamp(entry.timestamp.into()).timestamp(),
973        })
974        .into_response(),
975      )
976    })
977  }
978
979  async fn status(
980    Extension(server_config): Extension<Arc<ServerConfig>>,
981    Extension(index): Extension<Arc<Index>>,
982    AcceptJson(accept_json): AcceptJson,
983  ) -> ServerResult {
984    task::block_in_place(|| {
985      Ok(if accept_json {
986        Json(index.status()?).into_response()
987      } else {
988        index.status()?.page(server_config).into_response()
989      })
990    })
991  }
992
993  async fn search_by_query(
994    Extension(index): Extension<Arc<Index>>,
995    Query(search): Query<Search>,
996  ) -> ServerResult<Redirect> {
997    Self::search(index, search.query).await
998  }
999
1000  async fn search_by_path(
1001    Extension(index): Extension<Arc<Index>>,
1002    Path(search): Path<Search>,
1003  ) -> ServerResult<Redirect> {
1004    Self::search(index, search.query).await
1005  }
1006
1007  async fn search(index: Arc<Index>, query: String) -> ServerResult<Redirect> {
1008    Self::search_inner(index, query).await
1009  }
1010
1011  async fn search_inner(index: Arc<Index>, query: String) -> ServerResult<Redirect> {
1012    task::block_in_place(|| {
1013      let query = query.trim();
1014
1015      if re::HASH.is_match(query) {
1016        if index.block_header(query.parse().unwrap())?.is_some() {
1017          Ok(Redirect::to(&format!("/block/{query}")))
1018        } else {
1019          Ok(Redirect::to(&format!("/tx/{query}")))
1020        }
1021      } else if re::OUTPOINT.is_match(query) {
1022        Ok(Redirect::to(&format!("/output/{query}")))
1023      } else if re::INSCRIPTION_ID.is_match(query) || re::INSCRIPTION_NUMBER.is_match(query) {
1024        Ok(Redirect::to(&format!("/inscription/{query}")))
1025      } else if re::SPACED_RUNE.is_match(query) {
1026        Ok(Redirect::to(&format!("/rune/{query}")))
1027      } else if re::RUNE_ID.is_match(query) {
1028        let id = query
1029          .parse::<RuneId>()
1030          .map_err(|err| ServerError::BadRequest(err.to_string()))?;
1031
1032        let rune = index.get_rune_by_id(id)?.ok_or_not_found(|| "rune ID")?;
1033
1034        Ok(Redirect::to(&format!("/rune/{rune}")))
1035      } else {
1036        Ok(Redirect::to(&format!("/sat/{query}")))
1037      }
1038    })
1039  }
1040
1041  async fn favicon() -> ServerResult {
1042    Ok(
1043      Self::static_asset(Path("/favicon.png".to_string()))
1044        .await
1045        .into_response(),
1046    )
1047  }
1048
1049  async fn feed(
1050    Extension(server_config): Extension<Arc<ServerConfig>>,
1051    Extension(index): Extension<Arc<Index>>,
1052  ) -> ServerResult {
1053    task::block_in_place(|| {
1054      let mut builder = rss::ChannelBuilder::default();
1055
1056      let chain = server_config.chain;
1057      match chain {
1058        Chain::Mainnet => builder.title("Inscriptions".to_string()),
1059        _ => builder.title(format!("Inscriptions – {chain:?}")),
1060      };
1061
1062      builder.generator(Some("ord".to_string()));
1063
1064      for (number, id) in index.get_feed_inscriptions(300)? {
1065        builder.item(
1066          rss::ItemBuilder::default()
1067            .title(Some(format!("Inscription {number}")))
1068            .link(Some(format!("/inscription/{id}")))
1069            .guid(Some(rss::Guid {
1070              value: format!("/inscription/{id}"),
1071              permalink: true,
1072            }))
1073            .build(),
1074        );
1075      }
1076
1077      Ok(
1078        (
1079          [
1080            (header::CONTENT_TYPE, "application/rss+xml"),
1081            (
1082              header::CONTENT_SECURITY_POLICY,
1083              "default-src 'unsafe-inline'",
1084            ),
1085          ],
1086          builder.build().to_string(),
1087        )
1088          .into_response(),
1089      )
1090    })
1091  }
1092
1093  async fn static_asset(Path(path): Path<String>) -> ServerResult {
1094    let content = StaticAssets::get(if let Some(stripped) = path.strip_prefix('/') {
1095      stripped
1096    } else {
1097      &path
1098    })
1099    .ok_or_not_found(|| format!("asset {path}"))?;
1100    let body = body::boxed(body::Full::from(content.data));
1101    let mime = mime_guess::from_path(path).first_or_octet_stream();
1102    Ok(
1103      Response::builder()
1104        .header(header::CONTENT_TYPE, mime.as_ref())
1105        .body(body)
1106        .unwrap(),
1107    )
1108  }
1109
1110  async fn block_count(Extension(index): Extension<Arc<Index>>) -> ServerResult<String> {
1111    task::block_in_place(|| Ok(index.block_count()?.to_string()))
1112  }
1113
1114  async fn block_height(Extension(index): Extension<Arc<Index>>) -> ServerResult<String> {
1115    task::block_in_place(|| {
1116      Ok(
1117        index
1118          .block_height()?
1119          .ok_or_not_found(|| "blockheight")?
1120          .to_string(),
1121      )
1122    })
1123  }
1124
1125  async fn block_hash(Extension(index): Extension<Arc<Index>>) -> ServerResult<String> {
1126    task::block_in_place(|| {
1127      Ok(
1128        index
1129          .block_hash(None)?
1130          .ok_or_not_found(|| "blockhash")?
1131          .to_string(),
1132      )
1133    })
1134  }
1135
1136  async fn block_hash_json(Extension(index): Extension<Arc<Index>>) -> ServerResult<Json<String>> {
1137    task::block_in_place(|| {
1138      Ok(Json(
1139        index
1140          .block_hash(None)?
1141          .ok_or_not_found(|| "blockhash")?
1142          .to_string(),
1143      ))
1144    })
1145  }
1146
1147  async fn block_hash_from_height(
1148    Extension(index): Extension<Arc<Index>>,
1149    Path(height): Path<u32>,
1150  ) -> ServerResult<String> {
1151    task::block_in_place(|| {
1152      Ok(
1153        index
1154          .block_hash(Some(height))?
1155          .ok_or_not_found(|| "blockhash")?
1156          .to_string(),
1157      )
1158    })
1159  }
1160
1161  async fn block_hash_from_height_json(
1162    Extension(index): Extension<Arc<Index>>,
1163    Path(height): Path<u32>,
1164  ) -> ServerResult<Json<String>> {
1165    task::block_in_place(|| {
1166      Ok(Json(
1167        index
1168          .block_hash(Some(height))?
1169          .ok_or_not_found(|| "blockhash")?
1170          .to_string(),
1171      ))
1172    })
1173  }
1174
1175  async fn block_info(
1176    Extension(index): Extension<Arc<Index>>,
1177    Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Block>>,
1178  ) -> ServerResult<Json<api::BlockInfo>> {
1179    task::block_in_place(|| {
1180      let hash = match query {
1181        query::Block::Hash(hash) => hash,
1182        query::Block::Height(height) => index
1183          .block_hash(Some(height))?
1184          .ok_or_not_found(|| format!("block {height}"))?,
1185      };
1186
1187      let header = index
1188        .block_header(hash)?
1189        .ok_or_not_found(|| format!("block {hash}"))?;
1190
1191      let info = index
1192        .block_header_info(hash)?
1193        .ok_or_not_found(|| format!("block {hash}"))?;
1194
1195      let stats = index
1196        .block_stats(info.height.try_into().unwrap())?
1197        .ok_or_not_found(|| format!("block {hash}"))?;
1198
1199      Ok(Json(api::BlockInfo {
1200        average_fee: stats.avg_fee.to_sat(),
1201        average_fee_rate: stats.avg_fee_rate.to_sat(),
1202        bits: header.bits.to_consensus(),
1203        chainwork: info.chainwork.try_into().unwrap(),
1204        confirmations: info.confirmations,
1205        difficulty: info.difficulty,
1206        hash,
1207        height: info.height.try_into().unwrap(),
1208        max_fee: stats.max_fee.to_sat(),
1209        max_fee_rate: stats.max_fee_rate.to_sat(),
1210        max_tx_size: stats.max_tx_size,
1211        median_fee: stats.median_fee.to_sat(),
1212        median_time: info
1213          .median_time
1214          .map(|median_time| median_time.try_into().unwrap()),
1215        merkle_root: info.merkle_root,
1216        min_fee: stats.min_fee.to_sat(),
1217        min_fee_rate: stats.min_fee_rate.to_sat(),
1218        next_block: info.next_block_hash,
1219        nonce: info.nonce,
1220        previous_block: info.previous_block_hash,
1221        subsidy: stats.subsidy.to_sat(),
1222        target: target_as_block_hash(header.target()),
1223        timestamp: info.time.try_into().unwrap(),
1224        total_fee: stats.total_fee.to_sat(),
1225        total_size: stats.total_size,
1226        total_weight: stats.total_weight,
1227        transaction_count: info.n_tx.try_into().unwrap(),
1228        #[allow(clippy::cast_sign_loss)]
1229        version: info.version.to_consensus() as u32,
1230      }))
1231    })
1232  }
1233
1234  async fn block_time(Extension(index): Extension<Arc<Index>>) -> ServerResult<String> {
1235    task::block_in_place(|| {
1236      Ok(
1237        index
1238          .block_time(index.block_height()?.ok_or_not_found(|| "blocktime")?)?
1239          .unix_timestamp()
1240          .to_string(),
1241      )
1242    })
1243  }
1244
1245  async fn input(
1246    Extension(server_config): Extension<Arc<ServerConfig>>,
1247    Extension(index): Extension<Arc<Index>>,
1248    Path(path): Path<(u32, usize, usize)>,
1249  ) -> ServerResult<PageHtml<InputHtml>> {
1250    task::block_in_place(|| {
1251      let not_found = || format!("input /{}/{}/{}", path.0, path.1, path.2);
1252
1253      let block = index
1254        .get_block_by_height(path.0)?
1255        .ok_or_not_found(not_found)?;
1256
1257      let transaction = block
1258        .txdata
1259        .into_iter()
1260        .nth(path.1)
1261        .ok_or_not_found(not_found)?;
1262
1263      let input = transaction
1264        .input
1265        .into_iter()
1266        .nth(path.2)
1267        .ok_or_not_found(not_found)?;
1268
1269      Ok(InputHtml { path, input }.page(server_config))
1270    })
1271  }
1272
1273  async fn faq() -> Redirect {
1274    Redirect::to("https://docs.ordinals.com/faq/")
1275  }
1276
1277  async fn bounties() -> Redirect {
1278    Redirect::to("https://docs.ordinals.com/bounty/")
1279  }
1280
1281  fn proxy_content(proxy: &Url, inscription_id: InscriptionId) -> ServerResult<Response> {
1282    let response = reqwest::blocking::Client::new()
1283      .get(format!("{}content/{}", proxy, inscription_id))
1284      .send()
1285      .map_err(|err| anyhow!(err))?;
1286
1287    let mut headers = response.headers().clone();
1288
1289    headers.insert(
1290      header::CONTENT_SECURITY_POLICY,
1291      HeaderValue::from_str(&format!(
1292        "default-src 'self' {proxy} 'unsafe-eval' 'unsafe-inline' data: blob:"
1293      ))
1294      .map_err(|err| ServerError::Internal(Error::from(err)))?,
1295    );
1296
1297    Ok(
1298      (
1299        response.status(),
1300        headers,
1301        response.bytes().map_err(|err| anyhow!(err))?,
1302      )
1303        .into_response(),
1304    )
1305  }
1306
1307  async fn content(
1308    Extension(index): Extension<Arc<Index>>,
1309    Extension(settings): Extension<Arc<Settings>>,
1310    Extension(server_config): Extension<Arc<ServerConfig>>,
1311    Path(inscription_id): Path<InscriptionId>,
1312    accept_encoding: AcceptEncoding,
1313  ) -> ServerResult {
1314    task::block_in_place(|| {
1315      if settings.is_hidden(inscription_id) {
1316        return Ok(PreviewUnknownHtml.into_response());
1317      }
1318
1319      let Some(mut inscription) = index.get_inscription_by_id(inscription_id)? else {
1320        return if let Some(proxy) = server_config.content_proxy.as_ref() {
1321          Self::proxy_content(proxy, inscription_id)
1322        } else {
1323          Err(ServerError::NotFound(format!(
1324            "{} not found",
1325            inscription_id
1326          )))
1327        };
1328      };
1329
1330      if let Some(delegate) = inscription.delegate() {
1331        inscription = index
1332          .get_inscription_by_id(delegate)?
1333          .ok_or_not_found(|| format!("delegate {inscription_id}"))?
1334      }
1335
1336      Ok(
1337        Self::content_response(inscription, accept_encoding, &server_config)?
1338          .ok_or_not_found(|| format!("inscription {inscription_id} content"))?
1339          .into_response(),
1340      )
1341    })
1342  }
1343
1344  fn content_response(
1345    inscription: Inscription,
1346    accept_encoding: AcceptEncoding,
1347    server_config: &ServerConfig,
1348  ) -> ServerResult<Option<(HeaderMap, Vec<u8>)>> {
1349    let mut headers = HeaderMap::new();
1350
1351    match &server_config.csp_origin {
1352      None => {
1353        headers.insert(
1354          header::CONTENT_SECURITY_POLICY,
1355          HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:"),
1356        );
1357        headers.append(
1358          header::CONTENT_SECURITY_POLICY,
1359          HeaderValue::from_static("default-src *:*/content/ *:*/blockheight *:*/blockhash *:*/blockhash/ *:*/blocktime *:*/r/ 'unsafe-eval' 'unsafe-inline' data: blob:"),
1360        );
1361      }
1362      Some(origin) => {
1363        let csp = format!("default-src {origin}/content/ {origin}/blockheight {origin}/blockhash {origin}/blockhash/ {origin}/blocktime {origin}/r/ 'unsafe-eval' 'unsafe-inline' data: blob:");
1364        headers.insert(
1365          header::CONTENT_SECURITY_POLICY,
1366          HeaderValue::from_str(&csp).map_err(|err| ServerError::Internal(Error::from(err)))?,
1367        );
1368      }
1369    }
1370
1371    headers.insert(
1372      header::CACHE_CONTROL,
1373      HeaderValue::from_static("public, max-age=1209600, immutable"),
1374    );
1375
1376    headers.insert(
1377      header::CONTENT_TYPE,
1378      inscription
1379        .content_type()
1380        .and_then(|content_type| content_type.parse().ok())
1381        .unwrap_or(HeaderValue::from_static("application/octet-stream")),
1382    );
1383
1384    if let Some(content_encoding) = inscription.content_encoding() {
1385      if accept_encoding.is_acceptable(&content_encoding) {
1386        headers.insert(header::CONTENT_ENCODING, content_encoding);
1387      } else if server_config.decompress && content_encoding == "br" {
1388        let Some(body) = inscription.into_body() else {
1389          return Ok(None);
1390        };
1391
1392        let mut decompressed = Vec::new();
1393
1394        Decompressor::new(body.as_slice(), 4096)
1395          .read_to_end(&mut decompressed)
1396          .map_err(|err| ServerError::Internal(err.into()))?;
1397
1398        return Ok(Some((headers, decompressed)));
1399      } else {
1400        return Err(ServerError::NotAcceptable {
1401          accept_encoding,
1402          content_encoding,
1403        });
1404      }
1405    }
1406
1407    let Some(body) = inscription.into_body() else {
1408      return Ok(None);
1409    };
1410
1411    Ok(Some((headers, body)))
1412  }
1413
1414  async fn preview(
1415    Extension(index): Extension<Arc<Index>>,
1416    Extension(settings): Extension<Arc<Settings>>,
1417    Extension(server_config): Extension<Arc<ServerConfig>>,
1418    Path(inscription_id): Path<InscriptionId>,
1419    accept_encoding: AcceptEncoding,
1420  ) -> ServerResult {
1421    task::block_in_place(|| {
1422      if settings.is_hidden(inscription_id) {
1423        return Ok(PreviewUnknownHtml.into_response());
1424      }
1425
1426      let mut inscription = index
1427        .get_inscription_by_id(inscription_id)?
1428        .ok_or_not_found(|| format!("inscription {inscription_id}"))?;
1429
1430      if let Some(delegate) = inscription.delegate() {
1431        inscription = index
1432          .get_inscription_by_id(delegate)?
1433          .ok_or_not_found(|| format!("delegate {inscription_id}"))?
1434      }
1435
1436      let media = inscription.media();
1437
1438      if let Media::Iframe = media {
1439        return Ok(
1440          Self::content_response(inscription, accept_encoding, &server_config)?
1441            .ok_or_not_found(|| format!("inscription {inscription_id} content"))?
1442            .into_response(),
1443        );
1444      }
1445
1446      let content_security_policy = server_config.preview_content_security_policy(media)?;
1447
1448      match media {
1449        Media::Audio => {
1450          Ok((content_security_policy, PreviewAudioHtml { inscription_id }).into_response())
1451        }
1452        Media::Code(language) => Ok(
1453          (
1454            content_security_policy,
1455            PreviewCodeHtml {
1456              inscription_id,
1457              language,
1458            },
1459          )
1460            .into_response(),
1461        ),
1462        Media::Font => {
1463          Ok((content_security_policy, PreviewFontHtml { inscription_id }).into_response())
1464        }
1465        Media::Iframe => unreachable!(),
1466        Media::Image(image_rendering) => Ok(
1467          (
1468            content_security_policy,
1469            PreviewImageHtml {
1470              image_rendering,
1471              inscription_id,
1472            },
1473          )
1474            .into_response(),
1475        ),
1476        Media::Markdown => Ok(
1477          (
1478            content_security_policy,
1479            PreviewMarkdownHtml { inscription_id },
1480          )
1481            .into_response(),
1482        ),
1483        Media::Model => {
1484          Ok((content_security_policy, PreviewModelHtml { inscription_id }).into_response())
1485        }
1486        Media::Pdf => {
1487          Ok((content_security_policy, PreviewPdfHtml { inscription_id }).into_response())
1488        }
1489        Media::Text => {
1490          Ok((content_security_policy, PreviewTextHtml { inscription_id }).into_response())
1491        }
1492        Media::Unknown => Ok((content_security_policy, PreviewUnknownHtml).into_response()),
1493        Media::Video => {
1494          Ok((content_security_policy, PreviewVideoHtml { inscription_id }).into_response())
1495        }
1496      }
1497    })
1498  }
1499
1500  async fn inscription(
1501    Extension(server_config): Extension<Arc<ServerConfig>>,
1502    Extension(index): Extension<Arc<Index>>,
1503    Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Inscription>>,
1504    AcceptJson(accept_json): AcceptJson,
1505  ) -> ServerResult {
1506    task::block_in_place(|| {
1507      if let query::Inscription::Sat(_) = query {
1508        if !index.has_sat_index() {
1509          return Err(ServerError::NotFound("sat index required".into()));
1510        }
1511      }
1512
1513      let (info, txout, inscription) = index
1514        .inscription_info(query)?
1515        .ok_or_not_found(|| format!("inscription {query}"))?;
1516
1517      Ok(if accept_json {
1518        Json(info).into_response()
1519      } else {
1520        InscriptionHtml {
1521          chain: server_config.chain,
1522          charms: Charm::Vindicated.unset(info.charms.iter().fold(0, |mut acc, charm| {
1523            charm.set(&mut acc);
1524            acc
1525          })),
1526          children: info.children,
1527          fee: info.fee,
1528          height: info.height,
1529          inscription,
1530          id: info.id,
1531          number: info.number,
1532          next: info.next,
1533          output: txout,
1534          parents: info.parents,
1535          previous: info.previous,
1536          rune: info.rune,
1537          sat: info.sat,
1538          satpoint: info.satpoint,
1539          timestamp: Utc.timestamp_opt(info.timestamp, 0).unwrap(),
1540        }
1541        .page(server_config)
1542        .into_response()
1543      })
1544    })
1545  }
1546
1547  async fn inscriptions_json(
1548    Extension(index): Extension<Arc<Index>>,
1549    AcceptJson(accept_json): AcceptJson,
1550    Json(inscriptions): Json<Vec<InscriptionId>>,
1551  ) -> ServerResult {
1552    task::block_in_place(|| {
1553      Ok(if accept_json {
1554        let mut response = Vec::new();
1555        for inscription in inscriptions {
1556          let query = query::Inscription::Id(inscription);
1557          let (info, _, _) = index
1558            .inscription_info(query)?
1559            .ok_or_not_found(|| format!("inscription {query}"))?;
1560
1561          response.push(info);
1562        }
1563
1564        Json(response).into_response()
1565      } else {
1566        StatusCode::NOT_FOUND.into_response()
1567      })
1568    })
1569  }
1570
1571  async fn collections(
1572    Extension(server_config): Extension<Arc<ServerConfig>>,
1573    Extension(index): Extension<Arc<Index>>,
1574  ) -> ServerResult {
1575    Self::collections_paginated(Extension(server_config), Extension(index), Path(0)).await
1576  }
1577
1578  async fn collections_paginated(
1579    Extension(server_config): Extension<Arc<ServerConfig>>,
1580    Extension(index): Extension<Arc<Index>>,
1581    Path(page_index): Path<usize>,
1582  ) -> ServerResult {
1583    task::block_in_place(|| {
1584      let (collections, more_collections) = index.get_collections_paginated(100, page_index)?;
1585
1586      let prev = page_index.checked_sub(1);
1587
1588      let next = more_collections.then_some(page_index + 1);
1589
1590      Ok(
1591        CollectionsHtml {
1592          inscriptions: collections,
1593          prev,
1594          next,
1595        }
1596        .page(server_config)
1597        .into_response(),
1598      )
1599    })
1600  }
1601
1602  async fn children(
1603    Extension(server_config): Extension<Arc<ServerConfig>>,
1604    Extension(index): Extension<Arc<Index>>,
1605    Path(inscription_id): Path<InscriptionId>,
1606  ) -> ServerResult {
1607    Self::children_paginated(
1608      Extension(server_config),
1609      Extension(index),
1610      Path((inscription_id, 0)),
1611    )
1612    .await
1613  }
1614
1615  async fn children_paginated(
1616    Extension(server_config): Extension<Arc<ServerConfig>>,
1617    Extension(index): Extension<Arc<Index>>,
1618    Path((parent, page)): Path<(InscriptionId, usize)>,
1619  ) -> ServerResult {
1620    task::block_in_place(|| {
1621      let entry = index
1622        .get_inscription_entry(parent)?
1623        .ok_or_not_found(|| format!("inscription {parent}"))?;
1624
1625      let parent_number = entry.inscription_number;
1626
1627      let (children, more_children) =
1628        index.get_children_by_sequence_number_paginated(entry.sequence_number, 100, page)?;
1629
1630      let prev_page = page.checked_sub(1);
1631
1632      let next_page = more_children.then_some(page + 1);
1633
1634      Ok(
1635        ChildrenHtml {
1636          parent,
1637          parent_number,
1638          children,
1639          prev_page,
1640          next_page,
1641        }
1642        .page(server_config)
1643        .into_response(),
1644      )
1645    })
1646  }
1647
1648  async fn children_recursive(
1649    Extension(index): Extension<Arc<Index>>,
1650    Path(inscription_id): Path<InscriptionId>,
1651  ) -> ServerResult {
1652    Self::children_recursive_paginated(Extension(index), Path((inscription_id, 0))).await
1653  }
1654
1655  async fn children_recursive_paginated(
1656    Extension(index): Extension<Arc<Index>>,
1657    Path((parent, page)): Path<(InscriptionId, usize)>,
1658  ) -> ServerResult {
1659    task::block_in_place(|| {
1660      let parent_sequence_number = index
1661        .get_inscription_entry(parent)?
1662        .ok_or_not_found(|| format!("inscription {parent}"))?
1663        .sequence_number;
1664
1665      let (ids, more) =
1666        index.get_children_by_sequence_number_paginated(parent_sequence_number, 100, page)?;
1667
1668      Ok(Json(api::Children { ids, more, page }).into_response())
1669    })
1670  }
1671
1672  async fn inscriptions(
1673    Extension(server_config): Extension<Arc<ServerConfig>>,
1674    Extension(index): Extension<Arc<Index>>,
1675    accept_json: AcceptJson,
1676  ) -> ServerResult {
1677    Self::inscriptions_paginated(
1678      Extension(server_config),
1679      Extension(index),
1680      Path(0),
1681      accept_json,
1682    )
1683    .await
1684  }
1685
1686  async fn inscriptions_paginated(
1687    Extension(server_config): Extension<Arc<ServerConfig>>,
1688    Extension(index): Extension<Arc<Index>>,
1689    Path(page_index): Path<u32>,
1690    AcceptJson(accept_json): AcceptJson,
1691  ) -> ServerResult {
1692    task::block_in_place(|| {
1693      let (inscriptions, more) = index.get_inscriptions_paginated(100, page_index)?;
1694
1695      let prev = page_index.checked_sub(1);
1696
1697      let next = more.then_some(page_index + 1);
1698
1699      Ok(if accept_json {
1700        Json(api::Inscriptions {
1701          ids: inscriptions,
1702          page_index,
1703          more,
1704        })
1705        .into_response()
1706      } else {
1707        InscriptionsHtml {
1708          inscriptions,
1709          next,
1710          prev,
1711        }
1712        .page(server_config)
1713        .into_response()
1714      })
1715    })
1716  }
1717
1718  async fn inscriptions_in_block(
1719    Extension(server_config): Extension<Arc<ServerConfig>>,
1720    Extension(index): Extension<Arc<Index>>,
1721    Path(block_height): Path<u32>,
1722    AcceptJson(accept_json): AcceptJson,
1723  ) -> ServerResult {
1724    Self::inscriptions_in_block_paginated(
1725      Extension(server_config),
1726      Extension(index),
1727      Path((block_height, 0)),
1728      AcceptJson(accept_json),
1729    )
1730    .await
1731  }
1732
1733  async fn inscriptions_in_block_paginated(
1734    Extension(server_config): Extension<Arc<ServerConfig>>,
1735    Extension(index): Extension<Arc<Index>>,
1736    Path((block_height, page_index)): Path<(u32, u32)>,
1737    AcceptJson(accept_json): AcceptJson,
1738  ) -> ServerResult {
1739    task::block_in_place(|| {
1740      let page_size = 100;
1741
1742      let page_index_usize = usize::try_from(page_index).unwrap_or(usize::MAX);
1743      let page_size_usize = usize::try_from(page_size).unwrap_or(usize::MAX);
1744
1745      let mut inscriptions = index
1746        .get_inscriptions_in_block(block_height)?
1747        .into_iter()
1748        .skip(page_index_usize.saturating_mul(page_size_usize))
1749        .take(page_size_usize.saturating_add(1))
1750        .collect::<Vec<InscriptionId>>();
1751
1752      let more = inscriptions.len() > page_size_usize;
1753
1754      if more {
1755        inscriptions.pop();
1756      }
1757
1758      Ok(if accept_json {
1759        Json(api::Inscriptions {
1760          ids: inscriptions,
1761          page_index,
1762          more,
1763        })
1764        .into_response()
1765      } else {
1766        InscriptionsBlockHtml::new(
1767          block_height,
1768          index.block_height()?.unwrap_or(Height(0)).n(),
1769          inscriptions,
1770          more,
1771          page_index,
1772        )?
1773        .page(server_config)
1774        .into_response()
1775      })
1776    })
1777  }
1778
1779  async fn parents(
1780    Extension(server_config): Extension<Arc<ServerConfig>>,
1781    Extension(index): Extension<Arc<Index>>,
1782    Path(inscription_id): Path<InscriptionId>,
1783  ) -> ServerResult<Response> {
1784    Self::parents_paginated(
1785      Extension(server_config),
1786      Extension(index),
1787      Path((inscription_id, 0)),
1788    )
1789    .await
1790  }
1791
1792  async fn parents_paginated(
1793    Extension(server_config): Extension<Arc<ServerConfig>>,
1794    Extension(index): Extension<Arc<Index>>,
1795    Path((id, page)): Path<(InscriptionId, usize)>,
1796  ) -> ServerResult<Response> {
1797    task::block_in_place(|| {
1798      let child = index
1799        .get_inscription_entry(id)?
1800        .ok_or_not_found(|| format!("inscription {id}"))?;
1801
1802      let (parents, more) = index.get_parents_by_sequence_number_paginated(child.parents, page)?;
1803
1804      let prev_page = page.checked_sub(1);
1805
1806      let next_page = more.then_some(page + 1);
1807
1808      Ok(
1809        ParentsHtml {
1810          id,
1811          number: child.inscription_number,
1812          parents,
1813          prev_page,
1814          next_page,
1815        }
1816        .page(server_config)
1817        .into_response(),
1818      )
1819    })
1820  }
1821
1822  async fn sat_inscriptions(
1823    Extension(index): Extension<Arc<Index>>,
1824    Path(sat): Path<u64>,
1825  ) -> ServerResult<Json<api::SatInscriptions>> {
1826    Self::sat_inscriptions_paginated(Extension(index), Path((sat, 0))).await
1827  }
1828
1829  async fn sat_inscriptions_paginated(
1830    Extension(index): Extension<Arc<Index>>,
1831    Path((sat, page)): Path<(u64, u64)>,
1832  ) -> ServerResult<Json<api::SatInscriptions>> {
1833    task::block_in_place(|| {
1834      if !index.has_sat_index() {
1835        return Err(ServerError::NotFound(
1836          "this server has no sat index".to_string(),
1837        ));
1838      }
1839
1840      let (ids, more) = index.get_inscription_ids_by_sat_paginated(Sat(sat), 100, page)?;
1841
1842      Ok(Json(api::SatInscriptions { ids, more, page }))
1843    })
1844  }
1845
1846  async fn sat_inscription_at_index(
1847    Extension(index): Extension<Arc<Index>>,
1848    Path((DeserializeFromStr(sat), inscription_index)): Path<(DeserializeFromStr<Sat>, isize)>,
1849  ) -> ServerResult<Json<api::SatInscription>> {
1850    task::block_in_place(|| {
1851      if !index.has_sat_index() {
1852        return Err(ServerError::NotFound(
1853          "this server has no sat index".to_string(),
1854        ));
1855      }
1856
1857      let id = index.get_inscription_id_by_sat_indexed(sat, inscription_index)?;
1858
1859      Ok(Json(api::SatInscription { id }))
1860    })
1861  }
1862
1863  async fn redirect_http_to_https(
1864    Extension(mut destination): Extension<String>,
1865    uri: Uri,
1866  ) -> Redirect {
1867    if let Some(path_and_query) = uri.path_and_query() {
1868      destination.push_str(path_and_query.as_str());
1869    }
1870
1871    Redirect::to(&destination)
1872  }
1873}
1874
1875#[cfg(test)]
1876mod tests {
1877  use {
1878    super::*, reqwest::Url, serde::de::DeserializeOwned, std::net::TcpListener, tempfile::TempDir,
1879  };
1880
1881  const RUNE: u128 = 99246114928149462;
1882
1883  #[derive(Default)]
1884  struct Builder {
1885    core: Option<mockcore::Handle>,
1886    config: String,
1887    ord_args: BTreeMap<String, Option<String>>,
1888    server_args: BTreeMap<String, Option<String>>,
1889  }
1890
1891  impl Builder {
1892    fn core(self, core: mockcore::Handle) -> Self {
1893      Self {
1894        core: Some(core),
1895        ..self
1896      }
1897    }
1898
1899    fn ord_option(mut self, option: &str, value: &str) -> Self {
1900      self.ord_args.insert(option.into(), Some(value.into()));
1901      self
1902    }
1903
1904    fn ord_flag(mut self, flag: &str) -> Self {
1905      self.ord_args.insert(flag.into(), None);
1906      self
1907    }
1908
1909    fn server_option(mut self, option: &str, value: &str) -> Self {
1910      self.server_args.insert(option.into(), Some(value.into()));
1911      self
1912    }
1913
1914    fn server_flag(mut self, flag: &str) -> Self {
1915      self.server_args.insert(flag.into(), None);
1916      self
1917    }
1918
1919    fn chain(self, chain: Chain) -> Self {
1920      self.ord_option("--chain", &chain.to_string())
1921    }
1922
1923    fn config(self, config: &str) -> Self {
1924      Self {
1925        config: config.into(),
1926        ..self
1927      }
1928    }
1929
1930    fn build(self) -> TestServer {
1931      let core = self.core.unwrap_or_else(|| {
1932        mockcore::builder()
1933          .network(
1934            self
1935              .ord_args
1936              .get("--chain")
1937              .map(|chain| chain.as_ref().unwrap().parse::<Chain>().unwrap())
1938              .unwrap_or_default()
1939              .network(),
1940          )
1941          .build()
1942      });
1943
1944      let tempdir = TempDir::new().unwrap();
1945
1946      let cookiefile = tempdir.path().join("cookie");
1947
1948      fs::write(&cookiefile, "username:password").unwrap();
1949
1950      let port = TcpListener::bind("127.0.0.1:0")
1951        .unwrap()
1952        .local_addr()
1953        .unwrap()
1954        .port();
1955
1956      let mut args = vec!["ord".to_string()];
1957
1958      args.push("--bitcoin-rpc-url".into());
1959      args.push(core.url());
1960
1961      args.push("--cookie-file".into());
1962      args.push(cookiefile.to_str().unwrap().into());
1963
1964      args.push("--datadir".into());
1965      args.push(tempdir.path().to_str().unwrap().into());
1966
1967      if !self.ord_args.contains_key("--chain") {
1968        args.push("--chain".into());
1969        args.push(core.network());
1970      }
1971
1972      for (arg, value) in self.ord_args {
1973        args.push(arg);
1974
1975        if let Some(value) = value {
1976          args.push(value);
1977        }
1978      }
1979
1980      args.push("server".into());
1981
1982      args.push("--address".into());
1983      args.push("127.0.0.1".into());
1984
1985      args.push("--http-port".into());
1986      args.push(port.to_string());
1987
1988      args.push("--polling-interval".into());
1989      args.push("100ms".into());
1990
1991      for (arg, value) in self.server_args {
1992        args.push(arg);
1993
1994        if let Some(value) = value {
1995          args.push(value);
1996        }
1997      }
1998
1999      let arguments = Arguments::try_parse_from(args).unwrap();
2000
2001      let Subcommand::Server(server) = arguments.subcommand else {
2002        panic!("unexpected subcommand: {:?}", arguments.subcommand);
2003      };
2004
2005      let settings = Settings::from_options(arguments.options)
2006        .or(serde_yaml::from_str::<Settings>(&self.config).unwrap())
2007        .or_defaults()
2008        .unwrap();
2009
2010      let index = Arc::new(Index::open(&settings).unwrap());
2011      let ord_server_handle = Handle::new();
2012
2013      {
2014        let index = index.clone();
2015        let ord_server_handle = ord_server_handle.clone();
2016        thread::spawn(|| server.run(settings, index, ord_server_handle).unwrap());
2017      }
2018
2019      while index.statistic(crate::index::Statistic::Commits) == 0 {
2020        thread::sleep(Duration::from_millis(50));
2021      }
2022
2023      let client = reqwest::blocking::Client::builder()
2024        .redirect(reqwest::redirect::Policy::none())
2025        .build()
2026        .unwrap();
2027
2028      for i in 0.. {
2029        match client.get(format!("http://127.0.0.1:{port}/status")).send() {
2030          Ok(_) => break,
2031          Err(err) => {
2032            if i == 400 {
2033              panic!("ord server failed to start: {err}");
2034            }
2035          }
2036        }
2037
2038        thread::sleep(Duration::from_millis(50));
2039      }
2040
2041      TestServer {
2042        core,
2043        index,
2044        ord_server_handle,
2045        tempdir,
2046        url: Url::parse(&format!("http://127.0.0.1:{port}")).unwrap(),
2047      }
2048    }
2049
2050    fn https(self) -> Self {
2051      self.server_flag("--https")
2052    }
2053
2054    fn index_runes(self) -> Self {
2055      self.ord_flag("--index-runes")
2056    }
2057
2058    fn index_sats(self) -> Self {
2059      self.ord_flag("--index-sats")
2060    }
2061
2062    fn redirect_http_to_https(self) -> Self {
2063      self.server_flag("--redirect-http-to-https")
2064    }
2065  }
2066
2067  struct TestServer {
2068    core: mockcore::Handle,
2069    index: Arc<Index>,
2070    ord_server_handle: Handle,
2071    #[allow(unused)]
2072    tempdir: TempDir,
2073    url: Url,
2074  }
2075
2076  impl TestServer {
2077    fn builder() -> Builder {
2078      Default::default()
2079    }
2080
2081    fn new() -> Self {
2082      Builder::default().build()
2083    }
2084
2085    #[track_caller]
2086    pub(crate) fn etch(
2087      &self,
2088      runestone: Runestone,
2089      outputs: usize,
2090      witness: Option<Witness>,
2091    ) -> (Txid, RuneId) {
2092      let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap();
2093
2094      self.mine_blocks(1);
2095
2096      self.core.broadcast_tx(TransactionTemplate {
2097        inputs: &[(block_count, 0, 0, Default::default())],
2098        p2tr: true,
2099        ..default()
2100      });
2101
2102      self.mine_blocks((Runestone::COMMIT_CONFIRMATIONS - 1).into());
2103
2104      let witness = witness.unwrap_or_else(|| {
2105        let tapscript = script::Builder::new()
2106          .push_slice::<&PushBytes>(
2107            runestone
2108              .etching
2109              .unwrap()
2110              .rune
2111              .unwrap()
2112              .commitment()
2113              .as_slice()
2114              .try_into()
2115              .unwrap(),
2116          )
2117          .into_script();
2118        let mut witness = Witness::default();
2119        witness.push(tapscript);
2120        witness.push([]);
2121        witness
2122      });
2123
2124      let txid = self.core.broadcast_tx(TransactionTemplate {
2125        inputs: &[(block_count + 1, 1, 0, witness)],
2126        op_return: Some(runestone.encipher()),
2127        outputs,
2128        ..default()
2129      });
2130
2131      self.mine_blocks(1);
2132
2133      (
2134        txid,
2135        RuneId {
2136          block: (self.index.block_count().unwrap() - 1).into(),
2137          tx: 1,
2138        },
2139      )
2140    }
2141
2142    #[track_caller]
2143    fn get(&self, path: impl AsRef<str>) -> reqwest::blocking::Response {
2144      if let Err(error) = self.index.update() {
2145        log::error!("{error}");
2146      }
2147      reqwest::blocking::get(self.join_url(path.as_ref())).unwrap()
2148    }
2149
2150    #[track_caller]
2151    pub(crate) fn get_json<T: DeserializeOwned>(&self, path: impl AsRef<str>) -> T {
2152      if let Err(error) = self.index.update() {
2153        log::error!("{error}");
2154      }
2155
2156      let client = reqwest::blocking::Client::new();
2157
2158      let response = client
2159        .get(self.join_url(path.as_ref()))
2160        .header(header::ACCEPT, "application/json")
2161        .send()
2162        .unwrap();
2163
2164      assert_eq!(response.status(), StatusCode::OK);
2165
2166      response.json().unwrap()
2167    }
2168
2169    fn join_url(&self, url: &str) -> Url {
2170      self.url.join(url).unwrap()
2171    }
2172
2173    #[track_caller]
2174    fn assert_response(&self, path: impl AsRef<str>, status: StatusCode, expected_response: &str) {
2175      let response = self.get(path);
2176      assert_eq!(response.status(), status, "{}", response.text().unwrap());
2177      pretty_assert_eq!(response.text().unwrap(), expected_response);
2178    }
2179
2180    #[track_caller]
2181    fn assert_response_regex(
2182      &self,
2183      path: impl AsRef<str>,
2184      status: StatusCode,
2185      regex: impl AsRef<str>,
2186    ) {
2187      let response = self.get(path);
2188      assert_eq!(response.status(), status);
2189      assert_regex_match!(response.text().unwrap(), regex.as_ref());
2190    }
2191
2192    fn assert_response_csp(
2193      &self,
2194      path: impl AsRef<str>,
2195      status: StatusCode,
2196      content_security_policy: &str,
2197      regex: impl AsRef<str>,
2198    ) {
2199      let response = self.get(path);
2200      assert_eq!(response.status(), status);
2201      assert_eq!(
2202        response
2203          .headers()
2204          .get(header::CONTENT_SECURITY_POLICY,)
2205          .unwrap(),
2206        content_security_policy
2207      );
2208      assert_regex_match!(response.text().unwrap(), regex.as_ref());
2209    }
2210
2211    #[track_caller]
2212    fn assert_redirect(&self, path: &str, location: &str) {
2213      let response = reqwest::blocking::Client::builder()
2214        .redirect(reqwest::redirect::Policy::none())
2215        .build()
2216        .unwrap()
2217        .get(self.join_url(path))
2218        .send()
2219        .unwrap();
2220
2221      assert_eq!(response.status(), StatusCode::SEE_OTHER);
2222      assert_eq!(response.headers().get(header::LOCATION).unwrap(), location);
2223    }
2224
2225    #[track_caller]
2226    fn mine_blocks(&self, n: u64) -> Vec<Block> {
2227      let blocks = self.core.mine_blocks(n);
2228      self.index.update().unwrap();
2229      blocks
2230    }
2231
2232    fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec<Block> {
2233      let blocks = self.core.mine_blocks_with_subsidy(n, subsidy);
2234      self.index.update().unwrap();
2235      blocks
2236    }
2237  }
2238
2239  impl Drop for TestServer {
2240    fn drop(&mut self) {
2241      self.ord_server_handle.shutdown();
2242    }
2243  }
2244
2245  fn parse_server_args(args: &str) -> (Settings, Server) {
2246    match Arguments::try_parse_from(args.split_whitespace()) {
2247      Ok(arguments) => match arguments.subcommand {
2248        Subcommand::Server(server) => (
2249          Settings::from_options(arguments.options)
2250            .or_defaults()
2251            .unwrap(),
2252          server,
2253        ),
2254        subcommand => panic!("unexpected subcommand: {subcommand:?}"),
2255      },
2256      Err(err) => panic!("error parsing arguments: {err}"),
2257    }
2258  }
2259
2260  #[test]
2261  fn http_and_https_port_dont_conflict() {
2262    parse_server_args(
2263      "ord server --http-port 0 --https-port 0 --acme-cache foo --acme-contact bar --acme-domain baz",
2264    );
2265  }
2266
2267  #[test]
2268  fn http_port_defaults_to_80() {
2269    assert_eq!(parse_server_args("ord server").1.http_port(), Some(80));
2270  }
2271
2272  #[test]
2273  fn https_port_defaults_to_none() {
2274    assert_eq!(parse_server_args("ord server").1.https_port(), None);
2275  }
2276
2277  #[test]
2278  fn https_sets_https_port_to_443() {
2279    assert_eq!(
2280      parse_server_args("ord server --https --acme-cache foo --acme-contact bar --acme-domain baz")
2281        .1
2282        .https_port(),
2283      Some(443)
2284    );
2285  }
2286
2287  #[test]
2288  fn https_disables_http() {
2289    assert_eq!(
2290      parse_server_args("ord server --https --acme-cache foo --acme-contact bar --acme-domain baz")
2291        .1
2292        .http_port(),
2293      None
2294    );
2295  }
2296
2297  #[test]
2298  fn https_port_disables_http() {
2299    assert_eq!(
2300      parse_server_args(
2301        "ord server --https-port 433 --acme-cache foo --acme-contact bar --acme-domain baz"
2302      )
2303      .1
2304      .http_port(),
2305      None
2306    );
2307  }
2308
2309  #[test]
2310  fn https_port_sets_https_port() {
2311    assert_eq!(
2312      parse_server_args(
2313        "ord server --https-port 1000 --acme-cache foo --acme-contact bar --acme-domain baz"
2314      )
2315      .1
2316      .https_port(),
2317      Some(1000)
2318    );
2319  }
2320
2321  #[test]
2322  fn http_with_https_leaves_http_enabled() {
2323    assert_eq!(
2324      parse_server_args(
2325        "ord server --https --http --acme-cache foo --acme-contact bar --acme-domain baz"
2326      )
2327      .1
2328      .http_port(),
2329      Some(80)
2330    );
2331  }
2332
2333  #[test]
2334  fn http_with_https_leaves_https_enabled() {
2335    assert_eq!(
2336      parse_server_args(
2337        "ord server --https --http --acme-cache foo --acme-contact bar --acme-domain baz"
2338      )
2339      .1
2340      .https_port(),
2341      Some(443)
2342    );
2343  }
2344
2345  #[test]
2346  fn acme_contact_accepts_multiple_values() {
2347    assert!(Arguments::try_parse_from([
2348      "ord",
2349      "server",
2350      "--address",
2351      "127.0.0.1",
2352      "--http-port",
2353      "0",
2354      "--acme-contact",
2355      "foo",
2356      "--acme-contact",
2357      "bar"
2358    ])
2359    .is_ok());
2360  }
2361
2362  #[test]
2363  fn acme_domain_accepts_multiple_values() {
2364    assert!(Arguments::try_parse_from([
2365      "ord",
2366      "server",
2367      "--address",
2368      "127.0.0.1",
2369      "--http-port",
2370      "0",
2371      "--acme-domain",
2372      "foo",
2373      "--acme-domain",
2374      "bar"
2375    ])
2376    .is_ok());
2377  }
2378
2379  #[test]
2380  fn acme_cache_defaults_to_data_dir() {
2381    let arguments = Arguments::try_parse_from(["ord", "--datadir", "foo", "server"]).unwrap();
2382
2383    let settings = Settings::from_options(arguments.options)
2384      .or_defaults()
2385      .unwrap();
2386
2387    let acme_cache = Server::acme_cache(None, &settings).display().to_string();
2388    assert!(
2389      acme_cache.contains(if cfg!(windows) {
2390        r"foo\acme-cache"
2391      } else {
2392        "foo/acme-cache"
2393      }),
2394      "{acme_cache}"
2395    )
2396  }
2397
2398  #[test]
2399  fn acme_cache_flag_is_respected() {
2400    let arguments =
2401      Arguments::try_parse_from(["ord", "--datadir", "foo", "server", "--acme-cache", "bar"])
2402        .unwrap();
2403
2404    let settings = Settings::from_options(arguments.options)
2405      .or_defaults()
2406      .unwrap();
2407
2408    let acme_cache = Server::acme_cache(Some(&"bar".into()), &settings)
2409      .display()
2410      .to_string();
2411    assert_eq!(acme_cache, "bar")
2412  }
2413
2414  #[test]
2415  fn acme_domain_defaults_to_hostname() {
2416    let (_, server) = parse_server_args("ord server");
2417    assert_eq!(
2418      server.acme_domains().unwrap(),
2419      &[System::host_name().unwrap()]
2420    );
2421  }
2422
2423  #[test]
2424  fn acme_domain_flag_is_respected() {
2425    let (_, server) = parse_server_args("ord server --acme-domain example.com");
2426    assert_eq!(server.acme_domains().unwrap(), &["example.com"]);
2427  }
2428
2429  #[test]
2430  fn install_sh_redirects_to_github() {
2431    TestServer::new().assert_redirect(
2432      "/install.sh",
2433      "https://raw.githubusercontent.com/ordinals/ord/master/install.sh",
2434    );
2435  }
2436
2437  #[test]
2438  fn ordinal_redirects_to_sat() {
2439    TestServer::new().assert_redirect("/ordinal/0", "/sat/0");
2440  }
2441
2442  #[test]
2443  fn bounties_redirects_to_docs_site() {
2444    TestServer::new().assert_redirect("/bounties", "https://docs.ordinals.com/bounty/");
2445  }
2446
2447  #[test]
2448  fn faq_redirects_to_docs_site() {
2449    TestServer::new().assert_redirect("/faq", "https://docs.ordinals.com/faq/");
2450  }
2451
2452  #[test]
2453  fn search_by_query_returns_rune() {
2454    TestServer::new().assert_redirect("/search?query=ABCD", "/rune/ABCD");
2455  }
2456
2457  #[test]
2458  fn search_by_query_returns_spaced_rune() {
2459    TestServer::new().assert_redirect("/search?query=AB•CD", "/rune/AB•CD");
2460  }
2461
2462  #[test]
2463  fn search_by_query_returns_inscription() {
2464    TestServer::new().assert_redirect(
2465      "/search?query=0000000000000000000000000000000000000000000000000000000000000000i0",
2466      "/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
2467    );
2468  }
2469
2470  #[test]
2471  fn search_by_query_returns_inscription_by_number() {
2472    TestServer::new().assert_redirect("/search?query=0", "/inscription/0");
2473  }
2474
2475  #[test]
2476  fn search_is_whitespace_insensitive() {
2477    TestServer::new().assert_redirect("/search/ abc ", "/sat/abc");
2478  }
2479
2480  #[test]
2481  fn search_by_path_returns_sat() {
2482    TestServer::new().assert_redirect("/search/abc", "/sat/abc");
2483  }
2484
2485  #[test]
2486  fn search_for_blockhash_returns_block() {
2487    TestServer::new().assert_redirect(
2488      "/search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
2489      "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
2490    );
2491  }
2492
2493  #[test]
2494  fn search_for_txid_returns_transaction() {
2495    TestServer::new().assert_redirect(
2496      "/search/0000000000000000000000000000000000000000000000000000000000000000",
2497      "/tx/0000000000000000000000000000000000000000000000000000000000000000",
2498    );
2499  }
2500
2501  #[test]
2502  fn search_for_outpoint_returns_output() {
2503    TestServer::new().assert_redirect(
2504      "/search/0000000000000000000000000000000000000000000000000000000000000000:0",
2505      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
2506    );
2507  }
2508
2509  #[test]
2510  fn search_for_inscription_id_returns_inscription() {
2511    TestServer::new().assert_redirect(
2512      "/search/0000000000000000000000000000000000000000000000000000000000000000i0",
2513      "/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
2514    );
2515  }
2516
2517  #[test]
2518  fn search_by_path_returns_rune() {
2519    TestServer::new().assert_redirect("/search/ABCD", "/rune/ABCD");
2520  }
2521
2522  #[test]
2523  fn search_by_path_returns_spaced_rune() {
2524    TestServer::new().assert_redirect("/search/AB•CD", "/rune/AB•CD");
2525  }
2526
2527  #[test]
2528  fn search_by_rune_id_returns_rune() {
2529    let server = TestServer::builder()
2530      .chain(Chain::Regtest)
2531      .index_runes()
2532      .build();
2533
2534    server.mine_blocks(1);
2535
2536    let rune = Rune(RUNE);
2537
2538    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
2539
2540    server.etch(
2541      Runestone {
2542        edicts: vec![Edict {
2543          id: RuneId::default(),
2544          amount: u128::MAX,
2545          output: 0,
2546        }],
2547        etching: Some(Etching {
2548          rune: Some(rune),
2549          ..default()
2550        }),
2551        ..default()
2552      },
2553      1,
2554      None,
2555    );
2556
2557    server.mine_blocks(1);
2558
2559    server.assert_redirect("/search/8:1", "/rune/AAAAAAAAAAAAA");
2560    server.assert_redirect("/search?query=8:1", "/rune/AAAAAAAAAAAAA");
2561
2562    server.assert_response_regex(
2563      "/search/100000000000000000000:200000000000000000",
2564      StatusCode::BAD_REQUEST,
2565      ".*",
2566    );
2567  }
2568
2569  #[test]
2570  fn html_runes_balances_not_found() {
2571    TestServer::builder()
2572      .chain(Chain::Regtest)
2573      .build()
2574      .assert_response("/runes/balances", StatusCode::NOT_FOUND, "");
2575  }
2576
2577  #[test]
2578  fn fallback() {
2579    let server = TestServer::new();
2580
2581    server.assert_redirect("/0", "/inscription/0");
2582    server.assert_redirect("/0/", "/inscription/0");
2583    server.assert_redirect("/0//", "/inscription/0");
2584    server.assert_redirect(
2585      "/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0",
2586      "/inscription/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0",
2587    );
2588    server.assert_redirect("/-1", "/inscription/-1");
2589    server.assert_redirect("/FOO", "/rune/FOO");
2590    server.assert_redirect("/FO.O", "/rune/FO.O");
2591    server.assert_redirect("/FO•O", "/rune/FO•O");
2592    server.assert_redirect("/0:0", "/rune/0:0");
2593    server.assert_redirect(
2594      "/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
2595      "/output/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
2596    );
2597    server.assert_redirect(
2598      "/search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
2599      "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
2600    );
2601    server.assert_redirect(
2602      "/search/0000000000000000000000000000000000000000000000000000000000000000",
2603      "/tx/0000000000000000000000000000000000000000000000000000000000000000",
2604    );
2605
2606    server.assert_response_regex("/hello", StatusCode::NOT_FOUND, "");
2607
2608    server.assert_response_regex(
2609      "/%C3%28",
2610      StatusCode::BAD_REQUEST,
2611      "invalid utf-8 sequence of 1 bytes from index 0",
2612    );
2613  }
2614
2615  #[test]
2616  fn runes_can_be_queried_by_rune_id() {
2617    let server = TestServer::builder()
2618      .chain(Chain::Regtest)
2619      .index_runes()
2620      .build();
2621
2622    server.mine_blocks(1);
2623
2624    let rune = Rune(RUNE);
2625
2626    server.assert_response_regex("/rune/9:1", StatusCode::NOT_FOUND, ".*");
2627
2628    server.etch(
2629      Runestone {
2630        edicts: vec![Edict {
2631          id: RuneId::default(),
2632          amount: u128::MAX,
2633          output: 0,
2634        }],
2635        etching: Some(Etching {
2636          rune: Some(rune),
2637          ..default()
2638        }),
2639        ..default()
2640      },
2641      1,
2642      None,
2643    );
2644
2645    server.mine_blocks(1);
2646
2647    server.assert_response_regex(
2648      "/rune/8:1",
2649      StatusCode::OK,
2650      ".*<title>Rune AAAAAAAAAAAAA</title>.*",
2651    );
2652  }
2653
2654  #[test]
2655  fn runes_can_be_queried_by_rune_number() {
2656    let server = TestServer::builder()
2657      .chain(Chain::Regtest)
2658      .index_runes()
2659      .build();
2660
2661    server.mine_blocks(1);
2662
2663    server.assert_response_regex("/rune/0", StatusCode::NOT_FOUND, ".*");
2664
2665    for i in 0..10 {
2666      let rune = Rune(RUNE + i);
2667      server.etch(
2668        Runestone {
2669          edicts: vec![Edict {
2670            id: RuneId::default(),
2671            amount: u128::MAX,
2672            output: 0,
2673          }],
2674          etching: Some(Etching {
2675            rune: Some(rune),
2676            ..default()
2677          }),
2678          ..default()
2679        },
2680        1,
2681        None,
2682      );
2683
2684      server.mine_blocks(1);
2685    }
2686
2687    server.assert_response_regex(
2688      "/rune/0",
2689      StatusCode::OK,
2690      ".*<title>Rune AAAAAAAAAAAAA</title>.*",
2691    );
2692
2693    for i in 1..6 {
2694      server.assert_response_regex(
2695        format!("/rune/{}", i),
2696        StatusCode::OK,
2697        ".*<title>Rune AAAAAAAAAAAA.*</title>.*",
2698      );
2699    }
2700
2701    server.assert_response_regex(
2702      "/rune/9",
2703      StatusCode::OK,
2704      ".*<title>Rune AAAAAAAAAAAAJ</title>.*",
2705    );
2706  }
2707
2708  #[test]
2709  fn runes_are_displayed_on_runes_page() {
2710    let server = TestServer::builder()
2711      .chain(Chain::Regtest)
2712      .index_runes()
2713      .build();
2714
2715    server.mine_blocks(1);
2716
2717    server.assert_response_regex(
2718      "/runes",
2719      StatusCode::OK,
2720      ".*<title>Runes</title>.*<h1>Runes</h1>\n<ul>\n</ul>\n<div class=center>\n    prev\n      next\n  </div>.*",
2721    );
2722
2723    let (txid, id) = server.etch(
2724      Runestone {
2725        edicts: vec![Edict {
2726          id: RuneId::default(),
2727          amount: u128::MAX,
2728          output: 0,
2729        }],
2730        etching: Some(Etching {
2731          rune: Some(Rune(RUNE)),
2732          symbol: Some('%'),
2733          premine: Some(u128::MAX),
2734          ..default()
2735        }),
2736        ..default()
2737      },
2738      1,
2739      Default::default(),
2740    );
2741
2742    pretty_assert_eq!(
2743      server.index.runes().unwrap(),
2744      [(
2745        id,
2746        RuneEntry {
2747          block: id.block,
2748          etching: txid,
2749          spaced_rune: SpacedRune {
2750            rune: Rune(RUNE),
2751            spacers: 0
2752          },
2753          premine: u128::MAX,
2754          timestamp: id.block,
2755          symbol: Some('%'),
2756          ..default()
2757        }
2758      )]
2759    );
2760
2761    assert_eq!(
2762      server.index.get_rune_balances().unwrap(),
2763      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
2764    );
2765
2766    server.assert_response_regex(
2767      "/runes",
2768      StatusCode::OK,
2769      ".*<title>Runes</title>.*
2770<h1>Runes</h1>
2771<ul>
2772  <li><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></li>
2773</ul>.*",
2774    );
2775  }
2776
2777  #[test]
2778  fn runes_are_displayed_on_rune_page() {
2779    let server = TestServer::builder()
2780      .chain(Chain::Regtest)
2781      .index_runes()
2782      .build();
2783
2784    server.mine_blocks(1);
2785
2786    let rune = Rune(RUNE);
2787
2788    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
2789
2790    let (txid, id) = server.etch(
2791      Runestone {
2792        edicts: vec![Edict {
2793          id: RuneId::default(),
2794          amount: u128::MAX,
2795          output: 0,
2796        }],
2797        etching: Some(Etching {
2798          rune: Some(rune),
2799          symbol: Some('%'),
2800          premine: Some(u128::MAX),
2801          turbo: true,
2802          ..default()
2803        }),
2804        ..default()
2805      },
2806      1,
2807      Some(
2808        Inscription {
2809          content_type: Some("text/plain".into()),
2810          body: Some("hello".into()),
2811          rune: Some(rune.commitment()),
2812          ..default()
2813        }
2814        .to_witness(),
2815      ),
2816    );
2817
2818    assert_eq!(
2819      server.index.runes().unwrap(),
2820      [(
2821        id,
2822        RuneEntry {
2823          block: id.block,
2824          etching: txid,
2825          spaced_rune: SpacedRune { rune, spacers: 0 },
2826          premine: u128::MAX,
2827          symbol: Some('%'),
2828          timestamp: id.block,
2829          turbo: true,
2830          ..default()
2831        }
2832      )]
2833    );
2834
2835    assert_eq!(
2836      server.index.get_rune_balances().unwrap(),
2837      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
2838    );
2839
2840    server.assert_response_regex(
2841      format!("/rune/{rune}"),
2842      StatusCode::OK,
2843      format!(
2844        ".*<title>Rune AAAAAAAAAAAAA</title>.*
2845<h1>AAAAAAAAAAAAA</h1>
2846.*<a.*<iframe .* src=/preview/{txid}i0></iframe></a>.*
2847<dl>
2848  <dt>number</dt>
2849  <dd>0</dd>
2850  <dt>timestamp</dt>
2851  <dd><time>1970-01-01 00:00:08 UTC</time></dd>
2852  <dt>id</dt>
2853  <dd>8:1</dd>
2854  <dt>etching block</dt>
2855  <dd><a href=/block/8>8</a></dd>
2856  <dt>etching transaction</dt>
2857  <dd>1</dd>
2858  <dt>mint</dt>
2859  <dd>no</dd>
2860  <dt>supply</dt>
2861  <dd>340282366920938463463374607431768211455\u{A0}%</dd>
2862  <dt>premine</dt>
2863  <dd>340282366920938463463374607431768211455\u{A0}%</dd>
2864  <dt>premine percentage</dt>
2865  <dd>100%</dd>
2866  <dt>burned</dt>
2867  <dd>0\u{A0}%</dd>
2868  <dt>divisibility</dt>
2869  <dd>0</dd>
2870  <dt>symbol</dt>
2871  <dd>%</dd>
2872  <dt>turbo</dt>
2873  <dd>true</dd>
2874  <dt>etching</dt>
2875  <dd><a class=monospace href=/tx/{txid}>{txid}</a></dd>
2876  <dt>parent</dt>
2877  <dd><a class=monospace href=/inscription/{txid}i0>{txid}i0</a></dd>
2878</dl>
2879.*"
2880      ),
2881    );
2882
2883    server.assert_response_regex(
2884      format!("/inscription/{txid}i0"),
2885      StatusCode::OK,
2886      ".*
2887<dl>
2888  <dt>rune</dt>
2889  <dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>
2890  .*
2891</dl>
2892.*",
2893    );
2894  }
2895
2896  #[test]
2897  fn etched_runes_are_displayed_on_block_page() {
2898    let server = TestServer::builder()
2899      .chain(Chain::Regtest)
2900      .index_runes()
2901      .build();
2902
2903    server.mine_blocks(1);
2904
2905    let rune0 = Rune(RUNE);
2906
2907    let (_txid, id) = server.etch(
2908      Runestone {
2909        edicts: vec![Edict {
2910          id: RuneId::default(),
2911          amount: u128::MAX,
2912          output: 0,
2913        }],
2914        etching: Some(Etching {
2915          rune: Some(rune0),
2916          ..default()
2917        }),
2918        ..default()
2919      },
2920      1,
2921      None,
2922    );
2923
2924    assert_eq!(
2925      server.index.get_runes_in_block(id.block - 1).unwrap().len(),
2926      0
2927    );
2928    assert_eq!(server.index.get_runes_in_block(id.block).unwrap().len(), 1);
2929    assert_eq!(
2930      server.index.get_runes_in_block(id.block + 1).unwrap().len(),
2931      0
2932    );
2933
2934    server.assert_response_regex(
2935      format!("/block/{}", id.block),
2936      StatusCode::OK,
2937      format!(".*<h2>1 Rune</h2>.*<li><a href=/rune/{rune0}>{rune0}</a></li>.*"),
2938    );
2939  }
2940
2941  #[test]
2942  fn runes_are_spaced() {
2943    let server = TestServer::builder()
2944      .chain(Chain::Regtest)
2945      .index_runes()
2946      .build();
2947
2948    server.mine_blocks(1);
2949
2950    let rune = Rune(RUNE);
2951
2952    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
2953
2954    let (txid, id) = server.etch(
2955      Runestone {
2956        edicts: vec![Edict {
2957          id: RuneId::default(),
2958          amount: u128::MAX,
2959          output: 0,
2960        }],
2961        etching: Some(Etching {
2962          rune: Some(rune),
2963          symbol: Some('%'),
2964          spacers: Some(1),
2965          premine: Some(u128::MAX),
2966          ..default()
2967        }),
2968        ..default()
2969      },
2970      1,
2971      Some(
2972        Inscription {
2973          content_type: Some("text/plain".into()),
2974          body: Some("hello".into()),
2975          rune: Some(rune.commitment()),
2976          ..default()
2977        }
2978        .to_witness(),
2979      ),
2980    );
2981
2982    pretty_assert_eq!(
2983      server.index.runes().unwrap(),
2984      [(
2985        id,
2986        RuneEntry {
2987          block: id.block,
2988          etching: txid,
2989          spaced_rune: SpacedRune { rune, spacers: 1 },
2990          premine: u128::MAX,
2991          symbol: Some('%'),
2992          timestamp: id.block,
2993          ..default()
2994        }
2995      )]
2996    );
2997
2998    assert_eq!(
2999      server.index.get_rune_balances().unwrap(),
3000      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
3001    );
3002
3003    server.assert_response_regex(
3004      format!("/rune/{rune}"),
3005      StatusCode::OK,
3006      r".*<title>Rune A•AAAAAAAAAAAA</title>.*<h1>A•AAAAAAAAAAAA</h1>.*",
3007    );
3008
3009    server.assert_response_regex(
3010      format!("/inscription/{txid}i0"),
3011      StatusCode::OK,
3012      ".*<dt>rune</dt>.*<dd><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></dd>.*",
3013    );
3014
3015    server.assert_response_regex(
3016      "/runes",
3017      StatusCode::OK,
3018      ".*<li><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></li>.*",
3019    );
3020
3021    server.assert_response_regex(
3022      format!("/tx/{txid}"),
3023      StatusCode::OK,
3024      ".*
3025  <dt>etching</dt>
3026  <dd><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></dd>
3027.*",
3028    );
3029
3030    server.assert_response_regex(
3031      format!("/output/{txid}:0"),
3032      StatusCode::OK,
3033      ".*<tr>
3034        <td><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></td>
3035        <td>340282366920938463463374607431768211455\u{A0}%</td>
3036      </tr>.*",
3037    );
3038  }
3039
3040  #[test]
3041  fn transactions_link_to_etching() {
3042    let server = TestServer::builder()
3043      .chain(Chain::Regtest)
3044      .index_runes()
3045      .build();
3046
3047    server.mine_blocks(1);
3048
3049    server.assert_response_regex(
3050      "/runes",
3051      StatusCode::OK,
3052      ".*<title>Runes</title>.*<h1>Runes</h1>\n<ul>\n</ul>.*",
3053    );
3054
3055    let (txid, id) = server.etch(
3056      Runestone {
3057        edicts: vec![Edict {
3058          id: RuneId::default(),
3059          amount: u128::MAX,
3060          output: 0,
3061        }],
3062        etching: Some(Etching {
3063          rune: Some(Rune(RUNE)),
3064          premine: Some(u128::MAX),
3065          ..default()
3066        }),
3067        ..default()
3068      },
3069      1,
3070      None,
3071    );
3072
3073    pretty_assert_eq!(
3074      server.index.runes().unwrap(),
3075      [(
3076        id,
3077        RuneEntry {
3078          block: id.block,
3079          etching: txid,
3080          spaced_rune: SpacedRune {
3081            rune: Rune(RUNE),
3082            spacers: 0
3083          },
3084          premine: u128::MAX,
3085          timestamp: id.block,
3086          ..default()
3087        }
3088      )]
3089    );
3090
3091    pretty_assert_eq!(
3092      server.index.get_rune_balances().unwrap(),
3093      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
3094    );
3095
3096    server.assert_response_regex(
3097      format!("/tx/{txid}"),
3098      StatusCode::OK,
3099      ".*
3100  <dt>etching</dt>
3101  <dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>
3102.*",
3103    );
3104  }
3105
3106  #[test]
3107  fn runes_are_displayed_on_output_page() {
3108    let server = TestServer::builder()
3109      .chain(Chain::Regtest)
3110      .index_runes()
3111      .build();
3112
3113    server.mine_blocks(1);
3114
3115    let rune = Rune(RUNE);
3116
3117    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
3118
3119    let (txid, id) = server.etch(
3120      Runestone {
3121        edicts: vec![Edict {
3122          id: RuneId::default(),
3123          amount: u128::MAX,
3124          output: 0,
3125        }],
3126        etching: Some(Etching {
3127          divisibility: Some(1),
3128          rune: Some(rune),
3129          premine: Some(u128::MAX),
3130          ..default()
3131        }),
3132        ..default()
3133      },
3134      1,
3135      None,
3136    );
3137
3138    pretty_assert_eq!(
3139      server.index.runes().unwrap(),
3140      [(
3141        id,
3142        RuneEntry {
3143          block: id.block,
3144          divisibility: 1,
3145          etching: txid,
3146          spaced_rune: SpacedRune { rune, spacers: 0 },
3147          premine: u128::MAX,
3148          timestamp: id.block,
3149          ..default()
3150        }
3151      )]
3152    );
3153
3154    let output = OutPoint { txid, vout: 0 };
3155
3156    assert_eq!(
3157      server.index.get_rune_balances().unwrap(),
3158      [(output, vec![(id, u128::MAX)])]
3159    );
3160
3161    server.assert_response_regex(
3162      format!("/output/{output}"),
3163      StatusCode::OK,
3164      format!(
3165        ".*<title>Output {output}</title>.*<h1>Output <span class=monospace>{output}</span></h1>.*
3166  <dt>runes</dt>
3167  <dd>
3168    <table>
3169      <tr>
3170        <th>rune</th>
3171        <th>balance</th>
3172      </tr>
3173      <tr>
3174        <td><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></td>
3175        <td>34028236692093846346337460743176821145.5\u{A0}¤</td>
3176      </tr>
3177    </table>
3178  </dd>
3179.*"
3180      ),
3181    );
3182
3183    let address = default_address(Chain::Regtest);
3184
3185    pretty_assert_eq!(
3186      server.get_json::<api::Output>(format!("/output/{output}")),
3187      api::Output {
3188        value: 5000000000,
3189        script_pubkey: address.script_pubkey().to_asm_string(),
3190        address: Some(uncheck(&address)),
3191        transaction: txid.to_string(),
3192        sat_ranges: None,
3193        indexed: true,
3194        inscriptions: Vec::new(),
3195        runes: vec![(
3196          SpacedRune {
3197            rune: Rune(RUNE),
3198            spacers: 0
3199          },
3200          Pile {
3201            amount: 340282366920938463463374607431768211455,
3202            divisibility: 1,
3203            symbol: None,
3204          }
3205        )],
3206        spent: false,
3207      }
3208    );
3209  }
3210
3211  #[test]
3212  fn http_to_https_redirect_with_path() {
3213    TestServer::builder()
3214      .redirect_http_to_https()
3215      .https()
3216      .build()
3217      .assert_redirect(
3218        "/sat/0",
3219        &format!("https://{}/sat/0", System::host_name().unwrap()),
3220      );
3221  }
3222
3223  #[test]
3224  fn http_to_https_redirect_with_empty() {
3225    TestServer::builder()
3226      .redirect_http_to_https()
3227      .https()
3228      .build()
3229      .assert_redirect("/", &format!("https://{}/", System::host_name().unwrap()));
3230  }
3231
3232  #[test]
3233  fn status() {
3234    let server = TestServer::builder().chain(Chain::Regtest).build();
3235
3236    server.mine_blocks(3);
3237
3238    server.core.broadcast_tx(TransactionTemplate {
3239      inputs: &[(
3240        1,
3241        0,
3242        0,
3243        inscription("text/plain;charset=utf-8", "hello").to_witness(),
3244      )],
3245      ..default()
3246    });
3247
3248    server.core.broadcast_tx(TransactionTemplate {
3249      inputs: &[(
3250        2,
3251        0,
3252        0,
3253        inscription("text/plain;charset=utf-8", "hello").to_witness(),
3254      )],
3255      ..default()
3256    });
3257
3258    server.core.broadcast_tx(TransactionTemplate {
3259      inputs: &[(
3260        3,
3261        0,
3262        0,
3263        Inscription {
3264          content_type: None,
3265          body: Some("hello".as_bytes().into()),
3266          ..default()
3267        }
3268        .to_witness(),
3269      )],
3270      ..default()
3271    });
3272
3273    server.mine_blocks(1);
3274
3275    server.assert_response_regex(
3276      "/status",
3277      StatusCode::OK,
3278      ".*<h1>Status</h1>
3279<dl>
3280  <dt>chain</dt>
3281  <dd>regtest</dd>
3282  <dt>height</dt>
3283  <dd><a href=/block/4>4</a></dd>
3284  <dt>inscriptions</dt>
3285  <dd><a href=/inscriptions>3</a></dd>
3286  <dt>blessed inscriptions</dt>
3287  <dd>3</dd>
3288  <dt>cursed inscriptions</dt>
3289  <dd>0</dd>
3290  <dt>runes</dt>
3291  <dd><a href=/runes>0</a></dd>
3292  <dt>lost sats</dt>
3293  <dd>.*</dd>
3294  <dt>started</dt>
3295  <dd>.*</dd>
3296  <dt>uptime</dt>
3297  <dd>.*</dd>
3298  <dt>minimum rune for next block</dt>
3299  <dd>.*</dd>
3300  <dt>version</dt>
3301  <dd>.*</dd>
3302  <dt>unrecoverably reorged</dt>
3303  <dd>false</dd>
3304  <dt>rune index</dt>
3305  <dd>false</dd>
3306  <dt>sat index</dt>
3307  <dd>false</dd>
3308  <dt>transaction index</dt>
3309  <dd>false</dd>
3310  <dt>git branch</dt>
3311  <dd>.*</dd>
3312  <dt>git commit</dt>
3313  <dd>
3314    <a href=https://github.com/ordinals/ord/commit/[[:xdigit:]]{40}>
3315      [[:xdigit:]]{40}
3316    </a>
3317  </dd>
3318  <dt>inscription content types</dt>
3319  <dd>
3320    <dl>
3321      <dt>text/plain;charset=utf-8</dt>
3322      <dd>2</dt>
3323      <dt><em>none</em></dt>
3324      <dd>1</dt>
3325    </dl>
3326  </dd>
3327</dl>
3328.*",
3329    );
3330  }
3331
3332  #[test]
3333  fn block_count_endpoint() {
3334    let test_server = TestServer::new();
3335
3336    let response = test_server.get("/blockcount");
3337
3338    assert_eq!(response.status(), StatusCode::OK);
3339    assert_eq!(response.text().unwrap(), "1");
3340
3341    test_server.mine_blocks(1);
3342
3343    let response = test_server.get("/blockcount");
3344
3345    assert_eq!(response.status(), StatusCode::OK);
3346    assert_eq!(response.text().unwrap(), "2");
3347  }
3348
3349  #[test]
3350  fn block_height_endpoint() {
3351    let test_server = TestServer::new();
3352
3353    let response = test_server.get("/blockheight");
3354
3355    assert_eq!(response.status(), StatusCode::OK);
3356    assert_eq!(response.text().unwrap(), "0");
3357
3358    test_server.mine_blocks(2);
3359
3360    let response = test_server.get("/blockheight");
3361
3362    assert_eq!(response.status(), StatusCode::OK);
3363    assert_eq!(response.text().unwrap(), "2");
3364  }
3365
3366  #[test]
3367  fn block_hash_endpoint() {
3368    let test_server = TestServer::new();
3369
3370    let response = test_server.get("/blockhash");
3371
3372    assert_eq!(response.status(), StatusCode::OK);
3373    assert_eq!(
3374      response.text().unwrap(),
3375      "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
3376    );
3377  }
3378
3379  #[test]
3380  fn block_hash_from_height_endpoint() {
3381    let test_server = TestServer::new();
3382
3383    let response = test_server.get("/blockhash/0");
3384
3385    assert_eq!(response.status(), StatusCode::OK);
3386    assert_eq!(
3387      response.text().unwrap(),
3388      "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
3389    );
3390  }
3391
3392  #[test]
3393  fn block_time_endpoint() {
3394    let test_server = TestServer::new();
3395
3396    let response = test_server.get("/blocktime");
3397
3398    assert_eq!(response.status(), StatusCode::OK);
3399    assert_eq!(response.text().unwrap(), "1231006505");
3400  }
3401
3402  #[test]
3403  fn range_end_before_range_start_returns_400() {
3404    TestServer::new().assert_response(
3405      "/range/1/0",
3406      StatusCode::BAD_REQUEST,
3407      "range start greater than range end",
3408    );
3409  }
3410
3411  #[test]
3412  fn invalid_range_start_returns_400() {
3413    TestServer::new().assert_response(
3414      "/range/=/0",
3415      StatusCode::BAD_REQUEST,
3416      "Invalid URL: failed to parse sat `=`: invalid integer: invalid digit found in string",
3417    );
3418  }
3419
3420  #[test]
3421  fn invalid_range_end_returns_400() {
3422    TestServer::new().assert_response(
3423      "/range/0/=",
3424      StatusCode::BAD_REQUEST,
3425      "Invalid URL: failed to parse sat `=`: invalid integer: invalid digit found in string",
3426    );
3427  }
3428
3429  #[test]
3430  fn empty_range_returns_400() {
3431    TestServer::new().assert_response("/range/0/0", StatusCode::BAD_REQUEST, "empty range");
3432  }
3433
3434  #[test]
3435  fn range() {
3436    TestServer::new().assert_response_regex(
3437      "/range/0/1",
3438      StatusCode::OK,
3439      r".*<title>Sat Range 0–1</title>.*<h1>Sat Range 0–1</h1>
3440<dl>
3441  <dt>value</dt><dd>1</dd>
3442  <dt>first</dt><dd><a href=/sat/0 class=mythic>0</a></dd>
3443</dl>.*",
3444    );
3445  }
3446  #[test]
3447  fn sat_number() {
3448    TestServer::new().assert_response_regex("/sat/0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
3449  }
3450
3451  #[test]
3452  fn sat_decimal() {
3453    TestServer::new().assert_response_regex("/sat/0.0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
3454  }
3455
3456  #[test]
3457  fn sat_degree() {
3458    TestServer::new().assert_response_regex("/sat/0°0′0″0‴", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
3459  }
3460
3461  #[test]
3462  fn sat_name() {
3463    TestServer::new().assert_response_regex(
3464      "/sat/nvtdijuwxlp",
3465      StatusCode::OK,
3466      ".*<h1>Sat 0</h1>.*",
3467    );
3468  }
3469
3470  #[test]
3471  fn sat() {
3472    TestServer::new().assert_response_regex(
3473      "/sat/0",
3474      StatusCode::OK,
3475      ".*<title>Sat 0</title>.*<h1>Sat 0</h1>.*",
3476    );
3477  }
3478
3479  #[test]
3480  fn block() {
3481    TestServer::new().assert_response_regex(
3482      "/block/0",
3483      StatusCode::OK,
3484      ".*<title>Block 0</title>.*<h1>Block 0</h1>.*",
3485    );
3486  }
3487
3488  #[test]
3489  fn sat_out_of_range() {
3490    TestServer::new().assert_response(
3491      "/sat/2099999997690000",
3492      StatusCode::BAD_REQUEST,
3493      "Invalid URL: failed to parse sat `2099999997690000`: invalid integer range",
3494    );
3495  }
3496
3497  #[test]
3498  fn invalid_outpoint_hash_returns_400() {
3499    TestServer::new().assert_response(
3500      "/output/foo:0",
3501      StatusCode::BAD_REQUEST,
3502      "Invalid URL: error parsing TXID",
3503    );
3504  }
3505
3506  #[test]
3507  fn output_with_sat_index() {
3508    let txid = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
3509    TestServer::builder()
3510      .index_sats()
3511      .build()
3512      .assert_response_regex(
3513        format!("/output/{txid}:0"),
3514        StatusCode::OK,
3515        format!(
3516          ".*<title>Output {txid}:0</title>.*<h1>Output <span class=monospace>{txid}:0</span></h1>
3517<dl>
3518  <dt>value</dt><dd>5000000000</dd>
3519  <dt>script pubkey</dt><dd class=monospace>OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG</dd>
3520  <dt>transaction</dt><dd><a class=monospace href=/tx/{txid}>{txid}</a></dd>
3521  <dt>spent</dt><dd>false</dd>
3522</dl>
3523<h2>1 Sat Range</h2>
3524<ul class=monospace>
3525  <li><a href=/range/0/5000000000 class=mythic>0–5000000000</a></li>
3526</ul>.*"
3527        ),
3528      );
3529  }
3530
3531  #[test]
3532  fn output_without_sat_index() {
3533    let txid = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
3534    TestServer::new().assert_response_regex(
3535      format!("/output/{txid}:0"),
3536      StatusCode::OK,
3537      format!(
3538        ".*<title>Output {txid}:0</title>.*<h1>Output <span class=monospace>{txid}:0</span></h1>
3539<dl>
3540  <dt>value</dt><dd>5000000000</dd>
3541  <dt>script pubkey</dt><dd class=monospace>OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG</dd>
3542  <dt>transaction</dt><dd><a class=monospace href=/tx/{txid}>{txid}</a></dd>
3543  <dt>spent</dt><dd>false</dd>
3544</dl>.*"
3545      ),
3546    );
3547  }
3548
3549  #[test]
3550  fn null_output_is_initially_empty() {
3551    let txid = "0000000000000000000000000000000000000000000000000000000000000000";
3552    TestServer::builder().index_sats().build().assert_response_regex(
3553      format!("/output/{txid}:4294967295"),
3554      StatusCode::OK,
3555      format!(
3556        ".*<title>Output {txid}:4294967295</title>.*<h1>Output <span class=monospace>{txid}:4294967295</span></h1>
3557<dl>
3558  <dt>value</dt><dd>0</dd>
3559  <dt>script pubkey</dt><dd class=monospace></dd>
3560  <dt>transaction</dt><dd><a class=monospace href=/tx/{txid}>{txid}</a></dd>
3561  <dt>spent</dt><dd>false</dd>
3562</dl>
3563<h2>0 Sat Ranges</h2>
3564<ul class=monospace>
3565</ul>.*"
3566      ),
3567    );
3568  }
3569
3570  #[test]
3571  fn null_output_receives_lost_sats() {
3572    let server = TestServer::builder().index_sats().build();
3573
3574    server.mine_blocks_with_subsidy(1, 0);
3575
3576    let txid = "0000000000000000000000000000000000000000000000000000000000000000";
3577
3578    server.assert_response_regex(
3579      format!("/output/{txid}:4294967295"),
3580      StatusCode::OK,
3581      format!(
3582        ".*<title>Output {txid}:4294967295</title>.*<h1>Output <span class=monospace>{txid}:4294967295</span></h1>
3583<dl>
3584  <dt>value</dt><dd>5000000000</dd>
3585  <dt>script pubkey</dt><dd class=monospace></dd>
3586  <dt>transaction</dt><dd><a class=monospace href=/tx/{txid}>{txid}</a></dd>
3587  <dt>spent</dt><dd>false</dd>
3588</dl>
3589<h2>1 Sat Range</h2>
3590<ul class=monospace>
3591  <li><a href=/range/5000000000/10000000000 class=uncommon>5000000000–10000000000</a></li>
3592</ul>.*"
3593      ),
3594    );
3595  }
3596
3597  #[test]
3598  fn unbound_output_receives_unbound_inscriptions() {
3599    let server = TestServer::builder()
3600      .chain(Chain::Regtest)
3601      .index_sats()
3602      .build();
3603
3604    server.mine_blocks(1);
3605
3606    server.core.broadcast_tx(TransactionTemplate {
3607      inputs: &[(1, 0, 0, Default::default())],
3608      fee: 50 * 100_000_000,
3609      ..default()
3610    });
3611
3612    server.mine_blocks(1);
3613
3614    let txid = server.core.broadcast_tx(TransactionTemplate {
3615      inputs: &[(
3616        2,
3617        1,
3618        0,
3619        inscription("text/plain;charset=utf-8", "hello").to_witness(),
3620      )],
3621      ..default()
3622    });
3623
3624    server.mine_blocks(1);
3625
3626    let inscription_id = InscriptionId { txid, index: 0 };
3627
3628    server.assert_response_regex(
3629      format!("/inscription/{}", inscription_id),
3630      StatusCode::OK,
3631      format!(
3632        ".*<dl>
3633  <dt>id</dt>
3634  <dd class=monospace>{inscription_id}</dd>.*<dt>output</dt>
3635  <dd><a class=monospace href=/output/0000000000000000000000000000000000000000000000000000000000000000:0>0000000000000000000000000000000000000000000000000000000000000000:0</a></dd>.*"
3636      ),
3637    );
3638
3639    server.assert_response_regex(
3640      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
3641      StatusCode::OK,
3642      ".*<h1>Output <span class=monospace>0000000000000000000000000000000000000000000000000000000000000000:0</span></h1>
3643<dl>
3644  <dt>inscriptions</dt>
3645  <dd class=thumbnails>
3646    <a href=/inscription/.*><iframe sandbox=allow-scripts scrolling=no loading=lazy src=/preview/.*></iframe></a>
3647  </dd>.*",
3648    );
3649  }
3650
3651  #[test]
3652  fn unbound_output_returns_200() {
3653    TestServer::new().assert_response_regex(
3654      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
3655      StatusCode::OK,
3656      ".*",
3657    );
3658  }
3659
3660  #[test]
3661  fn invalid_output_returns_400() {
3662    TestServer::new().assert_response(
3663      "/output/foo:0",
3664      StatusCode::BAD_REQUEST,
3665      "Invalid URL: error parsing TXID",
3666    );
3667  }
3668
3669  #[test]
3670  fn home() {
3671    let server = TestServer::builder().chain(Chain::Regtest).build();
3672
3673    server.mine_blocks(1);
3674
3675    let mut ids = Vec::new();
3676
3677    for i in 0..101 {
3678      let txid = server.core.broadcast_tx(TransactionTemplate {
3679        inputs: &[(i + 1, 0, 0, inscription("image/png", "hello").to_witness())],
3680        ..default()
3681      });
3682      ids.push(InscriptionId { txid, index: 0 });
3683      server.mine_blocks(1);
3684    }
3685
3686    server.core.broadcast_tx(TransactionTemplate {
3687      inputs: &[(102, 0, 0, inscription("text/plain", "{}").to_witness())],
3688      ..default()
3689    });
3690
3691    server.mine_blocks(1);
3692
3693    server.assert_response_regex(
3694      "/",
3695      StatusCode::OK,
3696      format!(
3697        r".*<title>Ordinals</title>.*
3698<h1>Latest Inscriptions</h1>
3699<div class=thumbnails>
3700  <a href=/inscription/{}>.*</a>
3701  (<a href=/inscription/[[:xdigit:]]{{64}}i0>.*</a>\s*){{99}}
3702</div>
3703.*
3704",
3705        ids[100]
3706      ),
3707    );
3708  }
3709
3710  #[test]
3711  fn blocks() {
3712    let test_server = TestServer::new();
3713
3714    test_server.mine_blocks(1);
3715
3716    test_server.assert_response_regex(
3717      "/blocks",
3718      StatusCode::OK,
3719      ".*<title>Blocks</title>.*
3720<h1>Blocks</h1>
3721<div class=block>
3722  <h2><a href=/block/1>Block 1</a></h2>
3723  <div class=thumbnails>
3724  </div>
3725</div>
3726<div class=block>
3727  <h2><a href=/block/0>Block 0</a></h2>
3728  <div class=thumbnails>
3729  </div>
3730</div>
3731</ol>.*",
3732    );
3733  }
3734
3735  #[test]
3736  fn nav_displays_chain() {
3737    TestServer::builder()
3738      .chain(Chain::Regtest)
3739      .build()
3740      .assert_response_regex(
3741        "/",
3742        StatusCode::OK,
3743        ".*<a href=/ title=home>Ordinals<sup>regtest</sup></a>.*",
3744      );
3745  }
3746
3747  #[test]
3748  fn blocks_block_limit() {
3749    let test_server = TestServer::new();
3750
3751    test_server.mine_blocks(101);
3752
3753    test_server.assert_response_regex(
3754      "/blocks",
3755      StatusCode::OK,
3756      ".*<ol start=96 reversed class=block-list>\n(  <li><a href=/block/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>\n){95}</ol>.*"
3757    );
3758  }
3759
3760  #[test]
3761  fn block_not_found() {
3762    TestServer::new().assert_response(
3763      "/block/467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16",
3764      StatusCode::NOT_FOUND,
3765      "block 467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16 not found",
3766    );
3767  }
3768
3769  #[test]
3770  fn unmined_sat() {
3771    TestServer::new().assert_response_regex(
3772      "/sat/0",
3773      StatusCode::OK,
3774      ".*<dt>timestamp</dt><dd><time>2009-01-03 18:15:05 UTC</time></dd>.*",
3775    );
3776  }
3777
3778  #[test]
3779  fn mined_sat() {
3780    TestServer::new().assert_response_regex(
3781      "/sat/5000000000",
3782      StatusCode::OK,
3783      ".*<dt>timestamp</dt><dd><time>.*</time> \\(expected\\)</dd>.*",
3784    );
3785  }
3786
3787  #[test]
3788  fn static_asset() {
3789    TestServer::new().assert_response_regex(
3790      "/static/index.css",
3791      StatusCode::OK,
3792      r".*\.rare \{
3793  background-color: var\(--rare\);
3794}.*",
3795    );
3796  }
3797
3798  #[test]
3799  fn favicon() {
3800    TestServer::new().assert_response_regex("/favicon.ico", StatusCode::OK, r".*");
3801  }
3802
3803  #[test]
3804  fn clock_updates() {
3805    let test_server = TestServer::new();
3806    test_server.assert_response_regex("/clock", StatusCode::OK, ".*<text.*>0</text>.*");
3807    test_server.mine_blocks(1);
3808    test_server.assert_response_regex("/clock", StatusCode::OK, ".*<text.*>1</text>.*");
3809  }
3810
3811  #[test]
3812  fn block_by_hash() {
3813    let test_server = TestServer::new();
3814
3815    test_server.mine_blocks(1);
3816    let transaction = TransactionTemplate {
3817      inputs: &[(1, 0, 0, Default::default())],
3818      fee: 0,
3819      ..default()
3820    };
3821    test_server.core.broadcast_tx(transaction);
3822    let block_hash = test_server.mine_blocks(1)[0].block_hash();
3823
3824    test_server.assert_response_regex(
3825      format!("/block/{block_hash}"),
3826      StatusCode::OK,
3827      ".*<h1>Block 2</h1>.*",
3828    );
3829  }
3830
3831  #[test]
3832  fn block_by_height() {
3833    let test_server = TestServer::new();
3834
3835    test_server.assert_response_regex("/block/0", StatusCode::OK, ".*<h1>Block 0</h1>.*");
3836  }
3837
3838  #[test]
3839  fn transaction() {
3840    let test_server = TestServer::new();
3841
3842    let coinbase_tx = test_server.mine_blocks(1)[0].txdata[0].clone();
3843    let txid = coinbase_tx.txid();
3844
3845    test_server.assert_response_regex(
3846      format!("/tx/{txid}"),
3847      StatusCode::OK,
3848      format!(
3849        ".*<title>Transaction {txid}</title>.*<h1>Transaction <span class=monospace>{txid}</span></h1>
3850<dl>
3851</dl>
3852<h2>1 Input</h2>
3853<ul>
3854  <li><a class=monospace href=/output/0000000000000000000000000000000000000000000000000000000000000000:4294967295>0000000000000000000000000000000000000000000000000000000000000000:4294967295</a></li>
3855</ul>
3856<h2>1 Output</h2>
3857<ul class=monospace>
3858  <li>
3859    <a href=/output/{txid}:0 class=monospace>
3860      {txid}:0
3861    </a>
3862    <dl>
3863      <dt>value</dt><dd>5000000000</dd>
3864      <dt>script pubkey</dt><dd class=monospace>.*</dd>
3865    </dl>
3866  </li>
3867</ul>.*"
3868      ),
3869    );
3870  }
3871
3872  #[test]
3873  fn detect_unrecoverable_reorg() {
3874    let test_server = TestServer::new();
3875
3876    test_server.mine_blocks(21);
3877
3878    test_server.assert_response_regex(
3879      "/status",
3880      StatusCode::OK,
3881      ".*<dt>unrecoverably reorged</dt>\n  <dd>false</dd>.*",
3882    );
3883
3884    for _ in 0..15 {
3885      test_server.core.invalidate_tip();
3886    }
3887
3888    test_server.core.mine_blocks(21);
3889
3890    test_server.assert_response_regex(
3891      "/status",
3892      StatusCode::OK,
3893      ".*<dt>unrecoverably reorged</dt>\n  <dd>true</dd>.*",
3894    );
3895  }
3896
3897  #[test]
3898  fn rare_with_sat_index() {
3899    TestServer::builder().index_sats().build().assert_response(
3900      "/rare.txt",
3901      StatusCode::OK,
3902      "sat\tsatpoint
39030\t4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0
3904",
3905    );
3906  }
3907
3908  #[test]
3909  fn rare_without_sat_index() {
3910    TestServer::new().assert_response(
3911      "/rare.txt",
3912      StatusCode::OK,
3913      "sat\tsatpoint
3914",
3915    );
3916  }
3917
3918  #[test]
3919  fn show_rare_txt_in_header_with_sat_index() {
3920    TestServer::builder()
3921      .index_sats()
3922      .build()
3923      .assert_response_regex(
3924        "/",
3925        StatusCode::OK,
3926        ".*
3927      <a href=/clock title=clock>.*</a>
3928      <a href=/rare.txt title=rare>.*</a>.*",
3929      );
3930  }
3931
3932  #[test]
3933  fn rare_sat_location() {
3934    TestServer::builder()
3935      .index_sats()
3936      .build()
3937      .assert_response_regex(
3938        "/sat/0",
3939        StatusCode::OK,
3940        ".*>4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0<.*",
3941      );
3942  }
3943
3944  #[test]
3945  fn dont_show_rare_txt_in_header_without_sat_index() {
3946    TestServer::new().assert_response_regex(
3947      "/",
3948      StatusCode::OK,
3949      ".*
3950      <a href=/clock title=clock>.*</a>
3951      <a href=https://docs.ordinals.com/.*",
3952    );
3953  }
3954
3955  #[test]
3956  fn input() {
3957    TestServer::new().assert_response_regex(
3958      "/input/0/0/0",
3959      StatusCode::OK,
3960      ".*<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>.*",
3961    );
3962  }
3963
3964  #[test]
3965  fn input_missing() {
3966    TestServer::new().assert_response(
3967      "/input/1/1/1",
3968      StatusCode::NOT_FOUND,
3969      "input /1/1/1 not found",
3970    );
3971  }
3972
3973  #[test]
3974  fn commits_are_tracked() {
3975    let server = TestServer::new();
3976
3977    thread::sleep(Duration::from_millis(100));
3978    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1);
3979
3980    let info = server.index.info().unwrap();
3981    assert_eq!(info.transactions.len(), 1);
3982    assert_eq!(info.transactions[0].starting_block_count, 0);
3983
3984    server.index.update().unwrap();
3985
3986    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1);
3987
3988    let info = server.index.info().unwrap();
3989    assert_eq!(info.transactions.len(), 1);
3990    assert_eq!(info.transactions[0].starting_block_count, 0);
3991
3992    server.mine_blocks(1);
3993
3994    thread::sleep(Duration::from_millis(10));
3995    server.index.update().unwrap();
3996
3997    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 2);
3998
3999    let info = server.index.info().unwrap();
4000    assert_eq!(info.transactions.len(), 2);
4001    assert_eq!(info.transactions[0].starting_block_count, 0);
4002    assert_eq!(info.transactions[1].starting_block_count, 1);
4003    assert!(
4004      info.transactions[1].starting_timestamp - info.transactions[0].starting_timestamp >= 10
4005    );
4006  }
4007
4008  #[test]
4009  fn outputs_traversed_are_tracked() {
4010    let server = TestServer::builder().index_sats().build();
4011
4012    assert_eq!(
4013      server
4014        .index
4015        .statistic(crate::index::Statistic::OutputsTraversed),
4016      1
4017    );
4018
4019    server.index.update().unwrap();
4020
4021    assert_eq!(
4022      server
4023        .index
4024        .statistic(crate::index::Statistic::OutputsTraversed),
4025      1
4026    );
4027
4028    server.mine_blocks(2);
4029
4030    server.index.update().unwrap();
4031
4032    assert_eq!(
4033      server
4034        .index
4035        .statistic(crate::index::Statistic::OutputsTraversed),
4036      3
4037    );
4038  }
4039
4040  #[test]
4041  fn coinbase_sat_ranges_are_tracked() {
4042    let server = TestServer::builder().index_sats().build();
4043
4044    assert_eq!(
4045      server.index.statistic(crate::index::Statistic::SatRanges),
4046      1
4047    );
4048
4049    server.mine_blocks(1);
4050
4051    assert_eq!(
4052      server.index.statistic(crate::index::Statistic::SatRanges),
4053      2
4054    );
4055
4056    server.mine_blocks(1);
4057
4058    assert_eq!(
4059      server.index.statistic(crate::index::Statistic::SatRanges),
4060      3
4061    );
4062  }
4063
4064  #[test]
4065  fn split_sat_ranges_are_tracked() {
4066    let server = TestServer::builder().index_sats().build();
4067
4068    assert_eq!(
4069      server.index.statistic(crate::index::Statistic::SatRanges),
4070      1
4071    );
4072
4073    server.mine_blocks(1);
4074    server.core.broadcast_tx(TransactionTemplate {
4075      inputs: &[(1, 0, 0, Default::default())],
4076      outputs: 2,
4077      fee: 0,
4078      ..default()
4079    });
4080    server.mine_blocks(1);
4081
4082    assert_eq!(
4083      server.index.statistic(crate::index::Statistic::SatRanges),
4084      4,
4085    );
4086  }
4087
4088  #[test]
4089  fn fee_sat_ranges_are_tracked() {
4090    let server = TestServer::builder().index_sats().build();
4091
4092    assert_eq!(
4093      server.index.statistic(crate::index::Statistic::SatRanges),
4094      1
4095    );
4096
4097    server.mine_blocks(1);
4098    server.core.broadcast_tx(TransactionTemplate {
4099      inputs: &[(1, 0, 0, Default::default())],
4100      outputs: 2,
4101      fee: 2,
4102      ..default()
4103    });
4104    server.mine_blocks(1);
4105
4106    assert_eq!(
4107      server.index.statistic(crate::index::Statistic::SatRanges),
4108      5,
4109    );
4110  }
4111
4112  #[test]
4113  fn content_response_no_content() {
4114    assert_eq!(
4115      Server::content_response(
4116        Inscription {
4117          content_type: Some("text/plain".as_bytes().to_vec()),
4118          body: None,
4119          ..default()
4120        },
4121        AcceptEncoding::default(),
4122        &ServerConfig::default(),
4123      )
4124      .unwrap(),
4125      None
4126    );
4127  }
4128
4129  #[test]
4130  fn content_response_with_content() {
4131    let (headers, body) = Server::content_response(
4132      Inscription {
4133        content_type: Some("text/plain".as_bytes().to_vec()),
4134        body: Some(vec![1, 2, 3]),
4135        ..default()
4136      },
4137      AcceptEncoding::default(),
4138      &ServerConfig::default(),
4139    )
4140    .unwrap()
4141    .unwrap();
4142
4143    assert_eq!(headers["content-type"], "text/plain");
4144    assert_eq!(body, vec![1, 2, 3]);
4145  }
4146
4147  #[test]
4148  fn content_security_policy_no_origin() {
4149    let (headers, _) = Server::content_response(
4150      Inscription {
4151        content_type: Some("text/plain".as_bytes().to_vec()),
4152        body: Some(vec![1, 2, 3]),
4153        ..default()
4154      },
4155      AcceptEncoding::default(),
4156      &ServerConfig::default(),
4157    )
4158    .unwrap()
4159    .unwrap();
4160
4161    assert_eq!(
4162      headers["content-security-policy"],
4163      HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:")
4164    );
4165  }
4166
4167  #[test]
4168  fn content_security_policy_with_origin() {
4169    let (headers, _) = Server::content_response(
4170      Inscription {
4171        content_type: Some("text/plain".as_bytes().to_vec()),
4172        body: Some(vec![1, 2, 3]),
4173        ..default()
4174      },
4175      AcceptEncoding::default(),
4176      &ServerConfig {
4177        csp_origin: Some("https://ordinals.com".into()),
4178        ..default()
4179      },
4180    )
4181    .unwrap()
4182    .unwrap();
4183
4184    assert_eq!(headers["content-security-policy"], HeaderValue::from_static("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:"));
4185  }
4186
4187  #[test]
4188  fn preview_content_security_policy() {
4189    {
4190      let server = TestServer::builder().chain(Chain::Regtest).build();
4191
4192      server.mine_blocks(1);
4193
4194      let txid = server.core.broadcast_tx(TransactionTemplate {
4195        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
4196        ..default()
4197      });
4198
4199      server.mine_blocks(1);
4200
4201      let inscription_id = InscriptionId { txid, index: 0 };
4202
4203      server.assert_response_csp(
4204        format!("/preview/{}", inscription_id),
4205        StatusCode::OK,
4206        "default-src 'self'",
4207        format!(".*<html lang=en data-inscription={}>.*", inscription_id),
4208      );
4209    }
4210
4211    {
4212      let server = TestServer::builder()
4213        .chain(Chain::Regtest)
4214        .server_option("--csp-origin", "https://ordinals.com")
4215        .build();
4216
4217      server.mine_blocks(1);
4218
4219      let txid = server.core.broadcast_tx(TransactionTemplate {
4220        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
4221        ..default()
4222      });
4223
4224      server.mine_blocks(1);
4225
4226      let inscription_id = InscriptionId { txid, index: 0 };
4227
4228      server.assert_response_csp(
4229        format!("/preview/{}", inscription_id),
4230        StatusCode::OK,
4231        "default-src https://ordinals.com",
4232        format!(".*<html lang=en data-inscription={}>.*", inscription_id),
4233      );
4234    }
4235  }
4236
4237  #[test]
4238  fn code_preview() {
4239    let server = TestServer::builder().chain(Chain::Regtest).build();
4240    server.mine_blocks(1);
4241
4242    let txid = server.core.broadcast_tx(TransactionTemplate {
4243      inputs: &[(
4244        1,
4245        0,
4246        0,
4247        inscription("text/javascript", "hello").to_witness(),
4248      )],
4249      ..default()
4250    });
4251    let inscription_id = InscriptionId { txid, index: 0 };
4252
4253    server.mine_blocks(1);
4254
4255    server.assert_response_regex(
4256      format!("/preview/{inscription_id}"),
4257      StatusCode::OK,
4258      format!(r".*<html lang=en data-inscription={inscription_id} data-language=javascript>.*"),
4259    );
4260  }
4261
4262  #[test]
4263  fn content_response_no_content_type() {
4264    let (headers, body) = Server::content_response(
4265      Inscription {
4266        content_type: None,
4267        body: Some(Vec::new()),
4268        ..default()
4269      },
4270      AcceptEncoding::default(),
4271      &ServerConfig::default(),
4272    )
4273    .unwrap()
4274    .unwrap();
4275
4276    assert_eq!(headers["content-type"], "application/octet-stream");
4277    assert!(body.is_empty());
4278  }
4279
4280  #[test]
4281  fn content_response_bad_content_type() {
4282    let (headers, body) = Server::content_response(
4283      Inscription {
4284        content_type: Some("\n".as_bytes().to_vec()),
4285        body: Some(Vec::new()),
4286        ..Default::default()
4287      },
4288      AcceptEncoding::default(),
4289      &ServerConfig::default(),
4290    )
4291    .unwrap()
4292    .unwrap();
4293
4294    assert_eq!(headers["content-type"], "application/octet-stream");
4295    assert!(body.is_empty());
4296  }
4297
4298  #[test]
4299  fn text_preview() {
4300    let server = TestServer::builder().chain(Chain::Regtest).build();
4301    server.mine_blocks(1);
4302
4303    let txid = server.core.broadcast_tx(TransactionTemplate {
4304      inputs: &[(
4305        1,
4306        0,
4307        0,
4308        inscription("text/plain;charset=utf-8", "hello").to_witness(),
4309      )],
4310      ..default()
4311    });
4312
4313    let inscription_id = InscriptionId { txid, index: 0 };
4314
4315    server.mine_blocks(1);
4316
4317    server.assert_response_csp(
4318      format!("/preview/{}", inscription_id),
4319      StatusCode::OK,
4320      "default-src 'self'",
4321      format!(".*<html lang=en data-inscription={}>.*", inscription_id),
4322    );
4323  }
4324
4325  #[test]
4326  fn audio_preview() {
4327    let server = TestServer::builder().chain(Chain::Regtest).build();
4328    server.mine_blocks(1);
4329
4330    let txid = server.core.broadcast_tx(TransactionTemplate {
4331      inputs: &[(1, 0, 0, inscription("audio/flac", "hello").to_witness())],
4332      ..default()
4333    });
4334    let inscription_id = InscriptionId { txid, index: 0 };
4335
4336    server.mine_blocks(1);
4337
4338    server.assert_response_regex(
4339      format!("/preview/{inscription_id}"),
4340      StatusCode::OK,
4341      format!(r".*<audio .*>\s*<source src=/content/{inscription_id}>.*"),
4342    );
4343  }
4344
4345  #[test]
4346  fn font_preview() {
4347    let server = TestServer::builder().chain(Chain::Regtest).build();
4348    server.mine_blocks(1);
4349
4350    let txid = server.core.broadcast_tx(TransactionTemplate {
4351      inputs: &[(1, 0, 0, inscription("font/ttf", "hello").to_witness())],
4352      ..default()
4353    });
4354    let inscription_id = InscriptionId { txid, index: 0 };
4355
4356    server.mine_blocks(1);
4357
4358    server.assert_response_regex(
4359      format!("/preview/{inscription_id}"),
4360      StatusCode::OK,
4361      format!(r".*src: url\(/content/{inscription_id}\).*"),
4362    );
4363  }
4364
4365  #[test]
4366  fn pdf_preview() {
4367    let server = TestServer::builder().chain(Chain::Regtest).build();
4368    server.mine_blocks(1);
4369
4370    let txid = server.core.broadcast_tx(TransactionTemplate {
4371      inputs: &[(
4372        1,
4373        0,
4374        0,
4375        inscription("application/pdf", "hello").to_witness(),
4376      )],
4377      ..default()
4378    });
4379    let inscription_id = InscriptionId { txid, index: 0 };
4380
4381    server.mine_blocks(1);
4382
4383    server.assert_response_regex(
4384      format!("/preview/{inscription_id}"),
4385      StatusCode::OK,
4386      format!(r".*<canvas data-inscription={inscription_id}></canvas>.*"),
4387    );
4388  }
4389
4390  #[test]
4391  fn markdown_preview() {
4392    let server = TestServer::builder().chain(Chain::Regtest).build();
4393    server.mine_blocks(1);
4394
4395    let txid = server.core.broadcast_tx(TransactionTemplate {
4396      inputs: &[(1, 0, 0, inscription("text/markdown", "hello").to_witness())],
4397      ..default()
4398    });
4399    let inscription_id = InscriptionId { txid, index: 0 };
4400
4401    server.mine_blocks(1);
4402
4403    server.assert_response_regex(
4404      format!("/preview/{inscription_id}"),
4405      StatusCode::OK,
4406      format!(r".*<html lang=en data-inscription={inscription_id}>.*"),
4407    );
4408  }
4409
4410  #[test]
4411  fn image_preview() {
4412    let server = TestServer::builder().chain(Chain::Regtest).build();
4413    server.mine_blocks(1);
4414
4415    let txid = server.core.broadcast_tx(TransactionTemplate {
4416      inputs: &[(1, 0, 0, inscription("image/png", "hello").to_witness())],
4417      ..default()
4418    });
4419    let inscription_id = InscriptionId { txid, index: 0 };
4420
4421    server.mine_blocks(1);
4422
4423    server.assert_response_csp(
4424      format!("/preview/{inscription_id}"),
4425      StatusCode::OK,
4426      "default-src 'self' 'unsafe-inline'",
4427      format!(r".*background-image: url\(/content/{inscription_id}\);.*"),
4428    );
4429  }
4430
4431  #[test]
4432  fn iframe_preview() {
4433    let server = TestServer::builder().chain(Chain::Regtest).build();
4434    server.mine_blocks(1);
4435
4436    let txid = server.core.broadcast_tx(TransactionTemplate {
4437      inputs: &[(
4438        1,
4439        0,
4440        0,
4441        inscription("text/html;charset=utf-8", "hello").to_witness(),
4442      )],
4443      ..default()
4444    });
4445
4446    server.mine_blocks(1);
4447
4448    server.assert_response_csp(
4449      format!("/preview/{}", InscriptionId { txid, index: 0 }),
4450      StatusCode::OK,
4451      "default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:",
4452      "hello",
4453    );
4454  }
4455
4456  #[test]
4457  fn unknown_preview() {
4458    let server = TestServer::builder().chain(Chain::Regtest).build();
4459    server.mine_blocks(1);
4460
4461    let txid = server.core.broadcast_tx(TransactionTemplate {
4462      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
4463      ..default()
4464    });
4465
4466    server.mine_blocks(1);
4467
4468    server.assert_response_csp(
4469      format!("/preview/{}", InscriptionId { txid, index: 0 }),
4470      StatusCode::OK,
4471      "default-src 'self'",
4472      fs::read_to_string("templates/preview-unknown.html").unwrap(),
4473    );
4474  }
4475
4476  #[test]
4477  fn video_preview() {
4478    let server = TestServer::builder().chain(Chain::Regtest).build();
4479    server.mine_blocks(1);
4480
4481    let txid = server.core.broadcast_tx(TransactionTemplate {
4482      inputs: &[(1, 0, 0, inscription("video/webm", "hello").to_witness())],
4483      ..default()
4484    });
4485    let inscription_id = InscriptionId { txid, index: 0 };
4486
4487    server.mine_blocks(1);
4488
4489    server.assert_response_regex(
4490      format!("/preview/{inscription_id}"),
4491      StatusCode::OK,
4492      format!(r".*<video .*>\s*<source src=/content/{inscription_id}>.*"),
4493    );
4494  }
4495
4496  #[test]
4497  fn inscription_page_title() {
4498    let server = TestServer::builder()
4499      .chain(Chain::Regtest)
4500      .index_sats()
4501      .build();
4502    server.mine_blocks(1);
4503
4504    let txid = server.core.broadcast_tx(TransactionTemplate {
4505      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
4506      ..default()
4507    });
4508
4509    server.mine_blocks(1);
4510
4511    server.assert_response_regex(
4512      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
4513      StatusCode::OK,
4514      ".*<title>Inscription 0</title>.*",
4515    );
4516  }
4517
4518  #[test]
4519  fn inscription_page_has_sat_when_sats_are_tracked() {
4520    let server = TestServer::builder()
4521      .chain(Chain::Regtest)
4522      .index_sats()
4523      .build();
4524    server.mine_blocks(1);
4525
4526    let txid = server.core.broadcast_tx(TransactionTemplate {
4527      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
4528      ..default()
4529    });
4530
4531    server.mine_blocks(1);
4532
4533    server.assert_response_regex(
4534      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
4535      StatusCode::OK,
4536      r".*<dt>sat</dt>\s*<dd><a href=/sat/5000000000>5000000000</a></dd>\s*<dt>preview</dt>.*",
4537    );
4538  }
4539
4540  #[test]
4541  fn inscriptions_can_be_looked_up_by_sat_name() {
4542    let server = TestServer::builder()
4543      .chain(Chain::Regtest)
4544      .index_sats()
4545      .build();
4546    server.mine_blocks(1);
4547
4548    server.core.broadcast_tx(TransactionTemplate {
4549      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
4550      ..default()
4551    });
4552
4553    server.mine_blocks(1);
4554
4555    server.assert_response_regex(
4556      format!("/inscription/{}", Sat(5000000000).name()),
4557      StatusCode::OK,
4558      ".*<title>Inscription 0</title.*",
4559    );
4560  }
4561
4562  #[test]
4563  fn inscriptions_can_be_looked_up_by_sat_name_with_letter_i() {
4564    let server = TestServer::builder()
4565      .chain(Chain::Regtest)
4566      .index_sats()
4567      .build();
4568    server.assert_response_regex("/inscription/i", StatusCode::NOT_FOUND, ".*");
4569  }
4570
4571  #[test]
4572  fn inscription_page_does_not_have_sat_when_sats_are_not_tracked() {
4573    let server = TestServer::builder().chain(Chain::Regtest).build();
4574    server.mine_blocks(1);
4575
4576    let txid = server.core.broadcast_tx(TransactionTemplate {
4577      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
4578      ..default()
4579    });
4580
4581    server.mine_blocks(1);
4582
4583    server.assert_response_regex(
4584      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
4585      StatusCode::OK,
4586      r".*<dt>value</dt>\s*<dd>5000000000</dd>\s*<dt>preview</dt>.*",
4587    );
4588  }
4589
4590  #[test]
4591  fn strict_transport_security_header_is_set() {
4592    assert_eq!(
4593      TestServer::new()
4594        .get("/status")
4595        .headers()
4596        .get(header::STRICT_TRANSPORT_SECURITY)
4597        .unwrap(),
4598      "max-age=31536000; includeSubDomains; preload",
4599    );
4600  }
4601
4602  #[test]
4603  fn feed() {
4604    let server = TestServer::builder()
4605      .chain(Chain::Regtest)
4606      .index_sats()
4607      .build();
4608    server.mine_blocks(1);
4609
4610    server.core.broadcast_tx(TransactionTemplate {
4611      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
4612      ..default()
4613    });
4614
4615    server.mine_blocks(1);
4616
4617    server.assert_response_regex(
4618      "/feed.xml",
4619      StatusCode::OK,
4620      ".*<title>Inscription 0</title>.*",
4621    );
4622  }
4623
4624  #[test]
4625  fn inscription_with_unknown_type_and_no_body_has_unknown_preview() {
4626    let server = TestServer::builder()
4627      .chain(Chain::Regtest)
4628      .index_sats()
4629      .build();
4630    server.mine_blocks(1);
4631
4632    let txid = server.core.broadcast_tx(TransactionTemplate {
4633      inputs: &[(
4634        1,
4635        0,
4636        0,
4637        Inscription {
4638          content_type: Some("foo/bar".as_bytes().to_vec()),
4639          body: None,
4640          ..default()
4641        }
4642        .to_witness(),
4643      )],
4644      ..default()
4645    });
4646
4647    let inscription_id = InscriptionId { txid, index: 0 };
4648
4649    server.mine_blocks(1);
4650
4651    server.assert_response(
4652      format!("/preview/{inscription_id}"),
4653      StatusCode::OK,
4654      &fs::read_to_string("templates/preview-unknown.html").unwrap(),
4655    );
4656  }
4657
4658  #[test]
4659  fn inscription_with_known_type_and_no_body_has_unknown_preview() {
4660    let server = TestServer::builder()
4661      .chain(Chain::Regtest)
4662      .index_sats()
4663      .build();
4664    server.mine_blocks(1);
4665
4666    let txid = server.core.broadcast_tx(TransactionTemplate {
4667      inputs: &[(
4668        1,
4669        0,
4670        0,
4671        Inscription {
4672          content_type: Some("image/png".as_bytes().to_vec()),
4673          body: None,
4674          ..default()
4675        }
4676        .to_witness(),
4677      )],
4678      ..default()
4679    });
4680
4681    let inscription_id = InscriptionId { txid, index: 0 };
4682
4683    server.mine_blocks(1);
4684
4685    server.assert_response(
4686      format!("/preview/{inscription_id}"),
4687      StatusCode::OK,
4688      &fs::read_to_string("templates/preview-unknown.html").unwrap(),
4689    );
4690  }
4691
4692  #[test]
4693  fn content_responses_have_cache_control_headers() {
4694    let server = TestServer::builder().chain(Chain::Regtest).build();
4695    server.mine_blocks(1);
4696
4697    let txid = server.core.broadcast_tx(TransactionTemplate {
4698      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
4699      ..default()
4700    });
4701
4702    server.mine_blocks(1);
4703
4704    let response = server.get(format!("/content/{}", InscriptionId { txid, index: 0 }));
4705
4706    assert_eq!(response.status(), StatusCode::OK);
4707    assert_eq!(
4708      response.headers().get(header::CACHE_CONTROL).unwrap(),
4709      "public, max-age=1209600, immutable"
4710    );
4711  }
4712
4713  #[test]
4714  fn error_content_responses_have_max_age_zero_cache_control_headers() {
4715    let server = TestServer::builder().chain(Chain::Regtest).build();
4716    let response =
4717      server.get("/content/6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0");
4718
4719    assert_eq!(response.status(), 404);
4720    assert_eq!(
4721      response.headers().get(header::CACHE_CONTROL).unwrap(),
4722      "no-store"
4723    );
4724  }
4725
4726  #[test]
4727  fn inscriptions_page_with_no_prev_or_next() {
4728    TestServer::builder()
4729      .chain(Chain::Regtest)
4730      .index_sats()
4731      .build()
4732      .assert_response_regex("/inscriptions", StatusCode::OK, ".*prev\nnext.*");
4733  }
4734
4735  #[test]
4736  fn inscriptions_page_with_no_next() {
4737    let server = TestServer::builder()
4738      .chain(Chain::Regtest)
4739      .index_sats()
4740      .build();
4741
4742    for i in 0..101 {
4743      server.mine_blocks(1);
4744      server.core.broadcast_tx(TransactionTemplate {
4745        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
4746        ..default()
4747      });
4748    }
4749
4750    server.mine_blocks(1);
4751
4752    server.assert_response_regex(
4753      "/inscriptions/1",
4754      StatusCode::OK,
4755      ".*<a class=prev href=/inscriptions/0>prev</a>\nnext.*",
4756    );
4757  }
4758
4759  #[test]
4760  fn inscriptions_page_with_no_prev() {
4761    let server = TestServer::builder()
4762      .chain(Chain::Regtest)
4763      .index_sats()
4764      .build();
4765
4766    for i in 0..101 {
4767      server.mine_blocks(1);
4768      server.core.broadcast_tx(TransactionTemplate {
4769        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
4770        ..default()
4771      });
4772    }
4773
4774    server.mine_blocks(1);
4775
4776    server.assert_response_regex(
4777      "/inscriptions/0",
4778      StatusCode::OK,
4779      ".*prev\n<a class=next href=/inscriptions/1>next</a>.*",
4780    );
4781  }
4782
4783  #[test]
4784  fn collections_page_prev_and_next() {
4785    let server = TestServer::builder()
4786      .chain(Chain::Regtest)
4787      .index_sats()
4788      .build();
4789
4790    let mut parent_ids = Vec::new();
4791
4792    for i in 0..101 {
4793      server.mine_blocks(1);
4794
4795      parent_ids.push(InscriptionId {
4796        txid: server.core.broadcast_tx(TransactionTemplate {
4797          inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
4798          ..default()
4799        }),
4800        index: 0,
4801      });
4802    }
4803
4804    for (i, parent_id) in parent_ids.iter().enumerate().take(101) {
4805      server.mine_blocks(1);
4806
4807      server.core.broadcast_tx(TransactionTemplate {
4808        inputs: &[
4809          (i + 2, 1, 0, Default::default()),
4810          (
4811            i + 102,
4812            0,
4813            0,
4814            Inscription {
4815              content_type: Some("text/plain".into()),
4816              body: Some("hello".into()),
4817              parents: vec![parent_id.value()],
4818              ..default()
4819            }
4820            .to_witness(),
4821          ),
4822        ],
4823        outputs: 2,
4824        output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
4825        ..default()
4826      });
4827    }
4828
4829    server.mine_blocks(1);
4830
4831    server.assert_response_regex(
4832      "/collections",
4833      StatusCode::OK,
4834      r".*
4835<h1>Collections</h1>
4836<div class=thumbnails>
4837  <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
4838  (<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>\s*){99}
4839</div>
4840<div class=center>
4841prev
4842<a class=next href=/collections/1>next</a>
4843</div>.*"
4844        .to_string()
4845        .unindent(),
4846    );
4847
4848    server.assert_response_regex(
4849      "/collections/1",
4850      StatusCode::OK,
4851      ".*
4852<h1>Collections</h1>
4853<div class=thumbnails>
4854  <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
4855</div>
4856<div class=center>
4857<a class=prev href=/collections/0>prev</a>
4858next
4859</div>.*"
4860        .unindent(),
4861    );
4862  }
4863
4864  #[test]
4865  fn responses_are_gzipped() {
4866    let server = TestServer::new();
4867
4868    let mut headers = HeaderMap::new();
4869
4870    headers.insert(header::ACCEPT_ENCODING, "gzip".parse().unwrap());
4871
4872    let response = reqwest::blocking::Client::builder()
4873      .default_headers(headers)
4874      .build()
4875      .unwrap()
4876      .get(server.join_url("/"))
4877      .send()
4878      .unwrap();
4879
4880    assert_eq!(
4881      response.headers().get(header::CONTENT_ENCODING).unwrap(),
4882      "gzip"
4883    );
4884  }
4885
4886  #[test]
4887  fn responses_are_brotlied() {
4888    let server = TestServer::new();
4889
4890    let mut headers = HeaderMap::new();
4891
4892    headers.insert(header::ACCEPT_ENCODING, "br".parse().unwrap());
4893
4894    let response = reqwest::blocking::Client::builder()
4895      .default_headers(headers)
4896      .brotli(false)
4897      .build()
4898      .unwrap()
4899      .get(server.join_url("/"))
4900      .send()
4901      .unwrap();
4902
4903    assert_eq!(
4904      response.headers().get(header::CONTENT_ENCODING).unwrap(),
4905      "br"
4906    );
4907  }
4908
4909  #[test]
4910  fn inscription_links_to_parent() {
4911    let server = TestServer::builder().chain(Chain::Regtest).build();
4912    server.mine_blocks(1);
4913
4914    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
4915      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
4916      ..default()
4917    });
4918
4919    server.mine_blocks(1);
4920
4921    let parent_inscription_id = InscriptionId {
4922      txid: parent_txid,
4923      index: 0,
4924    };
4925
4926    let txid = server.core.broadcast_tx(TransactionTemplate {
4927      inputs: &[
4928        (
4929          2,
4930          0,
4931          0,
4932          Inscription {
4933            content_type: Some("text/plain".into()),
4934            body: Some("hello".into()),
4935            parents: vec![parent_inscription_id.value()],
4936            ..default()
4937          }
4938          .to_witness(),
4939        ),
4940        (2, 1, 0, Default::default()),
4941      ],
4942      ..default()
4943    });
4944
4945    server.mine_blocks(1);
4946
4947    let inscription_id = InscriptionId { txid, index: 0 };
4948
4949    server.assert_response_regex(
4950      format!("/inscription/{inscription_id}"),
4951      StatusCode::OK,
4952      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>.*"),
4953    );
4954    server.assert_response_regex(
4955      format!("/inscription/{parent_inscription_id}"),
4956      StatusCode::OK,
4957      format!(".*<title>Inscription 0</title>.*<dt>children</dt>.*<a href=/inscription/{inscription_id}>.*</a>.*"),
4958    );
4959
4960    assert_eq!(
4961      server
4962        .get_json::<api::Inscription>(format!("/inscription/{inscription_id}"))
4963        .parents,
4964      vec![parent_inscription_id],
4965    );
4966
4967    assert_eq!(
4968      server
4969        .get_json::<api::Inscription>(format!("/inscription/{parent_inscription_id}"))
4970        .children,
4971      [inscription_id],
4972    );
4973  }
4974
4975  #[test]
4976  fn inscription_with_and_without_children_page() {
4977    let server = TestServer::builder().chain(Chain::Regtest).build();
4978    server.mine_blocks(1);
4979
4980    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
4981      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
4982      ..default()
4983    });
4984
4985    server.mine_blocks(1);
4986
4987    let parent_inscription_id = InscriptionId {
4988      txid: parent_txid,
4989      index: 0,
4990    };
4991
4992    server.assert_response_regex(
4993      format!("/children/{parent_inscription_id}"),
4994      StatusCode::OK,
4995      ".*<h3>No children</h3>.*",
4996    );
4997
4998    let txid = server.core.broadcast_tx(TransactionTemplate {
4999      inputs: &[
5000        (
5001          2,
5002          0,
5003          0,
5004          Inscription {
5005            content_type: Some("text/plain".into()),
5006            body: Some("hello".into()),
5007            parents: vec![parent_inscription_id.value()],
5008            ..default()
5009          }
5010          .to_witness(),
5011        ),
5012        (2, 1, 0, Default::default()),
5013      ],
5014      ..default()
5015    });
5016
5017    server.mine_blocks(1);
5018
5019    let inscription_id = InscriptionId { txid, index: 0 };
5020
5021    server.assert_response_regex(
5022      format!("/children/{parent_inscription_id}"),
5023      StatusCode::OK,
5024      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>.*"),
5025    );
5026  }
5027
5028  #[test]
5029  fn inscriptions_page_shows_max_four_children() {
5030    let server = TestServer::builder().chain(Chain::Regtest).build();
5031    server.mine_blocks(1);
5032
5033    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
5034      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
5035      ..default()
5036    });
5037
5038    server.mine_blocks(6);
5039
5040    let parent_inscription_id = InscriptionId {
5041      txid: parent_txid,
5042      index: 0,
5043    };
5044
5045    let _txid = server.core.broadcast_tx(TransactionTemplate {
5046      inputs: &[
5047        (
5048          2,
5049          0,
5050          0,
5051          Inscription {
5052            content_type: Some("text/plain".into()),
5053            body: Some("hello".into()),
5054            parents: vec![parent_inscription_id.value()],
5055            ..default()
5056          }
5057          .to_witness(),
5058        ),
5059        (
5060          3,
5061          0,
5062          0,
5063          Inscription {
5064            content_type: Some("text/plain".into()),
5065            body: Some("hello".into()),
5066            parents: vec![parent_inscription_id.value()],
5067            ..default()
5068          }
5069          .to_witness(),
5070        ),
5071        (
5072          4,
5073          0,
5074          0,
5075          Inscription {
5076            content_type: Some("text/plain".into()),
5077            body: Some("hello".into()),
5078            parents: vec![parent_inscription_id.value()],
5079            ..default()
5080          }
5081          .to_witness(),
5082        ),
5083        (
5084          5,
5085          0,
5086          0,
5087          Inscription {
5088            content_type: Some("text/plain".into()),
5089            body: Some("hello".into()),
5090            parents: vec![parent_inscription_id.value()],
5091            ..default()
5092          }
5093          .to_witness(),
5094        ),
5095        (
5096          6,
5097          0,
5098          0,
5099          Inscription {
5100            content_type: Some("text/plain".into()),
5101            body: Some("hello".into()),
5102            parents: vec![parent_inscription_id.value()],
5103            ..default()
5104          }
5105          .to_witness(),
5106        ),
5107        (2, 1, 0, Default::default()),
5108      ],
5109      ..default()
5110    });
5111
5112    server.mine_blocks(1);
5113
5114    server.assert_response_regex(
5115      format!("/inscription/{parent_inscription_id}"),
5116      StatusCode::OK,
5117      format!(
5118        ".*<title>Inscription 0</title>.*
5119.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
5120.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
5121.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
5122.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
5123    <div class=center>
5124      <a href=/children/{parent_inscription_id}>all</a>
5125    </div>.*"
5126      ),
5127    );
5128  }
5129
5130  #[test]
5131  fn inscription_with_parent_page() {
5132    let server = TestServer::builder().chain(Chain::Regtest).build();
5133    server.mine_blocks(2);
5134
5135    let parent_a_txid = server.core.broadcast_tx(TransactionTemplate {
5136      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
5137      ..default()
5138    });
5139
5140    let parent_b_txid = server.core.broadcast_tx(TransactionTemplate {
5141      inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())],
5142      ..default()
5143    });
5144
5145    server.mine_blocks(1);
5146
5147    let parent_a_inscription_id = InscriptionId {
5148      txid: parent_a_txid,
5149      index: 0,
5150    };
5151
5152    let parent_b_inscription_id = InscriptionId {
5153      txid: parent_b_txid,
5154      index: 0,
5155    };
5156
5157    let txid = server.core.broadcast_tx(TransactionTemplate {
5158      inputs: &[
5159        (
5160          3,
5161          0,
5162          0,
5163          Inscription {
5164            content_type: Some("text/plain".into()),
5165            body: Some("hello".into()),
5166            parents: vec![
5167              parent_a_inscription_id.value(),
5168              parent_b_inscription_id.value(),
5169            ],
5170            ..default()
5171          }
5172          .to_witness(),
5173        ),
5174        (3, 1, 0, Default::default()),
5175        (3, 2, 0, Default::default()),
5176      ],
5177      ..default()
5178    });
5179
5180    server.mine_blocks(1);
5181
5182    let inscription_id = InscriptionId { txid, index: 0 };
5183
5184    server.assert_response_regex(
5185      format!("/parents/{inscription_id}"),
5186      StatusCode::OK,
5187      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>.*"),
5188    );
5189  }
5190
5191  #[test]
5192  fn inscription_parent_page_pagination() {
5193    let server = TestServer::builder().chain(Chain::Regtest).build();
5194
5195    server.mine_blocks(1);
5196
5197    let mut parent_ids = Vec::new();
5198    let mut inputs = Vec::new();
5199    for i in 0..101 {
5200      parent_ids.push(
5201        InscriptionId {
5202          txid: server.core.broadcast_tx(TransactionTemplate {
5203            inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
5204            ..default()
5205          }),
5206          index: 0,
5207        }
5208        .value(),
5209      );
5210
5211      inputs.push((i + 2, 1, 0, Witness::default()));
5212
5213      server.mine_blocks(1);
5214    }
5215
5216    inputs.insert(
5217      0,
5218      (
5219        102,
5220        0,
5221        0,
5222        Inscription {
5223          content_type: Some("text/plain".into()),
5224          body: Some("hello".into()),
5225          parents: parent_ids,
5226          ..default()
5227        }
5228        .to_witness(),
5229      ),
5230    );
5231
5232    let txid = server.core.broadcast_tx(TransactionTemplate {
5233      inputs: &inputs,
5234      ..default()
5235    });
5236
5237    server.mine_blocks(1);
5238
5239    let inscription_id = InscriptionId { txid, index: 0 };
5240
5241    server.assert_response_regex(
5242      format!("/parents/{inscription_id}"),
5243      StatusCode::OK,
5244      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}}.*"),
5245    );
5246
5247    server.assert_response_regex(
5248      format!("/parents/{inscription_id}/1"),
5249      StatusCode::OK,
5250      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}}.*"),
5251    );
5252
5253    server.assert_response_regex(
5254      format!("/inscription/{inscription_id}"),
5255      StatusCode::OK,
5256      ".*<title>Inscription -1</title>.*<h1>Inscription -1</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){4}.*",
5257    );
5258  }
5259
5260  #[test]
5261  fn inscription_number_endpoint() {
5262    let server = TestServer::builder().chain(Chain::Regtest).build();
5263    server.mine_blocks(2);
5264
5265    let txid = server.core.broadcast_tx(TransactionTemplate {
5266      inputs: &[
5267        (1, 0, 0, inscription("text/plain", "hello").to_witness()),
5268        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
5269      ],
5270      outputs: 2,
5271      ..default()
5272    });
5273
5274    let inscription_id = InscriptionId { txid, index: 0 };
5275    let cursed_inscription_id = InscriptionId { txid, index: 1 };
5276
5277    server.mine_blocks(1);
5278
5279    server.assert_response_regex(
5280      format!("/inscription/{inscription_id}"),
5281      StatusCode::OK,
5282      format!(
5283        ".*<h1>Inscription 0</h1>.*
5284<dl>
5285  <dt>id</dt>
5286  <dd class=monospace>{inscription_id}</dd>.*"
5287      ),
5288    );
5289    server.assert_response_regex(
5290      "/inscription/0",
5291      StatusCode::OK,
5292      format!(
5293        ".*<h1>Inscription 0</h1>.*
5294<dl>
5295  <dt>id</dt>
5296  <dd class=monospace>{inscription_id}</dd>.*"
5297      ),
5298    );
5299
5300    server.assert_response_regex(
5301      "/inscription/-1",
5302      StatusCode::OK,
5303      format!(
5304        ".*<h1>Inscription -1</h1>.*
5305<dl>
5306  <dt>id</dt>
5307  <dd class=monospace>{cursed_inscription_id}</dd>.*"
5308      ),
5309    )
5310  }
5311
5312  #[test]
5313  fn charm_cursed() {
5314    let server = TestServer::builder().chain(Chain::Regtest).build();
5315
5316    server.mine_blocks(2);
5317
5318    let txid = server.core.broadcast_tx(TransactionTemplate {
5319      inputs: &[
5320        (1, 0, 0, Witness::default()),
5321        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
5322      ],
5323      outputs: 2,
5324      ..default()
5325    });
5326
5327    let id = InscriptionId { txid, index: 0 };
5328
5329    server.mine_blocks(1);
5330
5331    server.assert_response_regex(
5332      format!("/inscription/{id}"),
5333      StatusCode::OK,
5334      format!(
5335        ".*<h1>Inscription -1</h1>.*
5336<dl>
5337  <dt>id</dt>
5338  <dd class=monospace>{id}</dd>
5339  <dt>charms</dt>
5340  <dd>
5341    <span title=cursed>👹</span>
5342  </dd>
5343  .*
5344</dl>
5345.*
5346"
5347      ),
5348    );
5349  }
5350
5351  #[test]
5352  fn charm_vindicated() {
5353    let server = TestServer::builder().chain(Chain::Regtest).build();
5354
5355    server.mine_blocks(110);
5356
5357    let txid = server.core.broadcast_tx(TransactionTemplate {
5358      inputs: &[
5359        (1, 0, 0, Witness::default()),
5360        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
5361      ],
5362      outputs: 2,
5363      ..default()
5364    });
5365
5366    let id = InscriptionId { txid, index: 0 };
5367
5368    server.mine_blocks(1);
5369
5370    server.assert_response_regex(
5371      format!("/inscription/{id}"),
5372      StatusCode::OK,
5373      format!(
5374        ".*<h1>Inscription 0</h1>.*
5375<dl>
5376  <dt>id</dt>
5377  <dd class=monospace>{id}</dd>
5378  .*
5379  <dt>value</dt>
5380  .*
5381</dl>
5382.*
5383"
5384      ),
5385    );
5386  }
5387
5388  #[test]
5389  fn charm_coin() {
5390    let server = TestServer::builder()
5391      .chain(Chain::Regtest)
5392      .index_sats()
5393      .build();
5394
5395    server.mine_blocks(2);
5396
5397    let txid = server.core.broadcast_tx(TransactionTemplate {
5398      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
5399      ..default()
5400    });
5401
5402    let id = InscriptionId { txid, index: 0 };
5403
5404    server.mine_blocks(1);
5405
5406    server.assert_response_regex(
5407      format!("/inscription/{id}"),
5408      StatusCode::OK,
5409      format!(
5410        ".*<h1>Inscription 0</h1>.*
5411<dl>
5412  <dt>id</dt>
5413  <dd class=monospace>{id}</dd>
5414  <dt>charms</dt>
5415  <dd>.*<span title=coin>🪙</span>.*</dd>
5416  .*
5417</dl>
5418.*
5419"
5420      ),
5421    );
5422  }
5423
5424  #[test]
5425  fn charm_uncommon() {
5426    let server = TestServer::builder()
5427      .chain(Chain::Regtest)
5428      .index_sats()
5429      .build();
5430
5431    server.mine_blocks(2);
5432
5433    let txid = server.core.broadcast_tx(TransactionTemplate {
5434      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
5435      ..default()
5436    });
5437
5438    let id = InscriptionId { txid, index: 0 };
5439
5440    server.mine_blocks(1);
5441
5442    server.assert_response_regex(
5443      format!("/inscription/{id}"),
5444      StatusCode::OK,
5445      format!(
5446        ".*<h1>Inscription 0</h1>.*
5447<dl>
5448  <dt>id</dt>
5449  <dd class=monospace>{id}</dd>
5450  <dt>charms</dt>
5451  <dd>.*<span title=uncommon>🌱</span>.*</dd>
5452  .*
5453</dl>
5454.*
5455"
5456      ),
5457    );
5458  }
5459
5460  #[test]
5461  fn charm_nineball() {
5462    let server = TestServer::builder()
5463      .chain(Chain::Regtest)
5464      .index_sats()
5465      .build();
5466
5467    server.mine_blocks(9);
5468
5469    let txid = server.core.broadcast_tx(TransactionTemplate {
5470      inputs: &[(9, 0, 0, inscription("text/plain", "foo").to_witness())],
5471      ..default()
5472    });
5473
5474    let id = InscriptionId { txid, index: 0 };
5475
5476    server.mine_blocks(1);
5477
5478    server.assert_response_regex(
5479      format!("/inscription/{id}"),
5480      StatusCode::OK,
5481      format!(
5482        ".*<h1>Inscription 0</h1>.*
5483<dl>
5484  <dt>id</dt>
5485  <dd class=monospace>{id}</dd>
5486  <dt>charms</dt>
5487  <dd>.*<span title=nineball>9️⃣</span>.*</dd>
5488  .*
5489</dl>
5490.*
5491"
5492      ),
5493    );
5494  }
5495
5496  #[test]
5497  fn charm_reinscription() {
5498    let server = TestServer::builder().chain(Chain::Regtest).build();
5499
5500    server.mine_blocks(1);
5501
5502    server.core.broadcast_tx(TransactionTemplate {
5503      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
5504      ..default()
5505    });
5506
5507    server.mine_blocks(1);
5508
5509    let txid = server.core.broadcast_tx(TransactionTemplate {
5510      inputs: &[(2, 1, 0, inscription("text/plain", "bar").to_witness())],
5511      ..default()
5512    });
5513
5514    server.mine_blocks(1);
5515
5516    let id = InscriptionId { txid, index: 0 };
5517
5518    server.assert_response_regex(
5519      format!("/inscription/{id}"),
5520      StatusCode::OK,
5521      format!(
5522        ".*<h1>Inscription -1</h1>.*
5523<dl>
5524  <dt>id</dt>
5525  <dd class=monospace>{id}</dd>
5526  <dt>charms</dt>
5527  <dd>
5528    <span title=reinscription>♻️</span>
5529    <span title=cursed>👹</span>
5530  </dd>
5531  .*
5532</dl>
5533.*
5534"
5535      ),
5536    );
5537  }
5538
5539  #[test]
5540  fn charm_reinscription_in_same_tx_input() {
5541    let server = TestServer::builder().chain(Chain::Regtest).build();
5542
5543    server.mine_blocks(1);
5544
5545    let script = script::Builder::new()
5546      .push_opcode(opcodes::OP_FALSE)
5547      .push_opcode(opcodes::all::OP_IF)
5548      .push_slice(b"ord")
5549      .push_slice([1])
5550      .push_slice(b"text/plain;charset=utf-8")
5551      .push_slice([])
5552      .push_slice(b"foo")
5553      .push_opcode(opcodes::all::OP_ENDIF)
5554      .push_opcode(opcodes::OP_FALSE)
5555      .push_opcode(opcodes::all::OP_IF)
5556      .push_slice(b"ord")
5557      .push_slice([1])
5558      .push_slice(b"text/plain;charset=utf-8")
5559      .push_slice([])
5560      .push_slice(b"bar")
5561      .push_opcode(opcodes::all::OP_ENDIF)
5562      .push_opcode(opcodes::OP_FALSE)
5563      .push_opcode(opcodes::all::OP_IF)
5564      .push_slice(b"ord")
5565      .push_slice([1])
5566      .push_slice(b"text/plain;charset=utf-8")
5567      .push_slice([])
5568      .push_slice(b"qix")
5569      .push_opcode(opcodes::all::OP_ENDIF)
5570      .into_script();
5571
5572    let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);
5573
5574    let txid = server.core.broadcast_tx(TransactionTemplate {
5575      inputs: &[(1, 0, 0, witness)],
5576      ..default()
5577    });
5578
5579    server.mine_blocks(1);
5580
5581    let id = InscriptionId { txid, index: 0 };
5582    server.assert_response_regex(
5583      format!("/inscription/{id}"),
5584      StatusCode::OK,
5585      format!(
5586        ".*<h1>Inscription 0</h1>.*
5587<dl>
5588  <dt>id</dt>
5589  <dd class=monospace>{id}</dd>
5590  .*
5591  <dt>value</dt>
5592  .*
5593</dl>
5594.*
5595"
5596      ),
5597    );
5598
5599    let id = InscriptionId { txid, index: 1 };
5600    server.assert_response_regex(
5601      format!("/inscription/{id}"),
5602      StatusCode::OK,
5603      ".*
5604    <span title=reinscription>♻️</span>
5605    <span title=cursed>👹</span>.*",
5606    );
5607
5608    let id = InscriptionId { txid, index: 2 };
5609    server.assert_response_regex(
5610      format!("/inscription/{id}"),
5611      StatusCode::OK,
5612      ".*
5613    <span title=reinscription>♻️</span>
5614    <span title=cursed>👹</span>.*",
5615    );
5616  }
5617
5618  #[test]
5619  fn charm_reinscription_in_same_tx_with_pointer() {
5620    let server = TestServer::builder().chain(Chain::Regtest).build();
5621
5622    server.mine_blocks(3);
5623
5624    let cursed_inscription = inscription("text/plain", "bar");
5625    let reinscription: Inscription = InscriptionTemplate {
5626      pointer: Some(0),
5627      ..default()
5628    }
5629    .into();
5630
5631    let txid = server.core.broadcast_tx(TransactionTemplate {
5632      inputs: &[
5633        (1, 0, 0, inscription("text/plain", "foo").to_witness()),
5634        (2, 0, 0, cursed_inscription.to_witness()),
5635        (3, 0, 0, reinscription.to_witness()),
5636      ],
5637      ..default()
5638    });
5639
5640    server.mine_blocks(1);
5641
5642    let id = InscriptionId { txid, index: 0 };
5643    server.assert_response_regex(
5644      format!("/inscription/{id}"),
5645      StatusCode::OK,
5646      format!(
5647        ".*<h1>Inscription 0</h1>.*
5648<dl>
5649  <dt>id</dt>
5650  <dd class=monospace>{id}</dd>
5651  .*
5652  <dt>value</dt>
5653  .*
5654</dl>
5655.*
5656"
5657      ),
5658    );
5659
5660    let id = InscriptionId { txid, index: 1 };
5661    server.assert_response_regex(
5662      format!("/inscription/{id}"),
5663      StatusCode::OK,
5664      ".*
5665    <span title=cursed>👹</span>.*",
5666    );
5667
5668    let id = InscriptionId { txid, index: 2 };
5669    server.assert_response_regex(
5670      format!("/inscription/{id}"),
5671      StatusCode::OK,
5672      ".*
5673    <span title=reinscription>♻️</span>
5674    <span title=cursed>👹</span>.*",
5675    );
5676  }
5677
5678  #[test]
5679  fn charm_unbound() {
5680    let server = TestServer::builder().chain(Chain::Regtest).build();
5681
5682    server.mine_blocks(1);
5683
5684    let txid = server.core.broadcast_tx(TransactionTemplate {
5685      inputs: &[(1, 0, 0, envelope(&[b"ord", &[128], &[0]]))],
5686      ..default()
5687    });
5688
5689    server.mine_blocks(1);
5690
5691    let id = InscriptionId { txid, index: 0 };
5692
5693    server.assert_response_regex(
5694      format!("/inscription/{id}"),
5695      StatusCode::OK,
5696      format!(
5697        ".*<h1>Inscription -1</h1>.*
5698<dl>
5699  <dt>id</dt>
5700  <dd class=monospace>{id}</dd>
5701  <dt>charms</dt>
5702  <dd>
5703    <span title=cursed>👹</span>
5704    <span title=unbound>🔓</span>
5705  </dd>
5706  .*
5707</dl>
5708.*
5709"
5710      ),
5711    );
5712  }
5713
5714  #[test]
5715  fn charm_lost() {
5716    let server = TestServer::builder().chain(Chain::Regtest).build();
5717
5718    server.mine_blocks(1);
5719
5720    let txid = server.core.broadcast_tx(TransactionTemplate {
5721      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
5722      ..default()
5723    });
5724
5725    let id = InscriptionId { txid, index: 0 };
5726
5727    server.mine_blocks(1);
5728
5729    server.assert_response_regex(
5730      format!("/inscription/{id}"),
5731      StatusCode::OK,
5732      format!(
5733        ".*<h1>Inscription 0</h1>.*
5734<dl>
5735  <dt>id</dt>
5736  <dd class=monospace>{id}</dd>
5737  .*
5738  <dt>value</dt>
5739  <dd>5000000000</dd>
5740  .*
5741</dl>
5742.*
5743"
5744      ),
5745    );
5746
5747    server.core.broadcast_tx(TransactionTemplate {
5748      inputs: &[(2, 1, 0, Default::default())],
5749      fee: 50 * COIN_VALUE,
5750      ..default()
5751    });
5752
5753    server.mine_blocks_with_subsidy(1, 0);
5754
5755    server.assert_response_regex(
5756      format!("/inscription/{id}"),
5757      StatusCode::OK,
5758      format!(
5759        ".*<h1>Inscription 0</h1>.*
5760<dl>
5761  <dt>id</dt>
5762  <dd class=monospace>{id}</dd>
5763  <dt>charms</dt>
5764  <dd>
5765    <span title=lost>🤔</span>
5766  </dd>
5767  .*
5768</dl>
5769.*
5770"
5771      ),
5772    );
5773  }
5774
5775  #[test]
5776  fn sat_recursive_endpoints() {
5777    let server = TestServer::builder()
5778      .chain(Chain::Regtest)
5779      .index_sats()
5780      .build();
5781
5782    assert_eq!(
5783      server.get_json::<api::SatInscriptions>("/r/sat/5000000000"),
5784      api::SatInscriptions {
5785        ids: Vec::new(),
5786        page: 0,
5787        more: false
5788      }
5789    );
5790
5791    assert_eq!(
5792      server.get_json::<api::SatInscription>("/r/sat/5000000000/at/0"),
5793      api::SatInscription { id: None }
5794    );
5795
5796    server.mine_blocks(1);
5797
5798    let txid = server.core.broadcast_tx(TransactionTemplate {
5799      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
5800      ..default()
5801    });
5802
5803    server.mine_blocks(1);
5804
5805    let mut ids = Vec::new();
5806    ids.push(InscriptionId { txid, index: 0 });
5807
5808    for i in 1..111 {
5809      let txid = server.core.broadcast_tx(TransactionTemplate {
5810        inputs: &[(i + 1, 1, 0, inscription("text/plain", "foo").to_witness())],
5811        ..default()
5812      });
5813
5814      server.mine_blocks(1);
5815
5816      ids.push(InscriptionId { txid, index: 0 });
5817    }
5818
5819    let paginated_response = server.get_json::<api::SatInscriptions>("/r/sat/5000000000");
5820
5821    let equivalent_paginated_response =
5822      server.get_json::<api::SatInscriptions>("/r/sat/5000000000/0");
5823
5824    assert_eq!(paginated_response.ids.len(), 100);
5825    assert!(paginated_response.more);
5826    assert_eq!(paginated_response.page, 0);
5827
5828    assert_eq!(
5829      paginated_response.ids.len(),
5830      equivalent_paginated_response.ids.len()
5831    );
5832    assert_eq!(paginated_response.more, equivalent_paginated_response.more);
5833    assert_eq!(paginated_response.page, equivalent_paginated_response.page);
5834
5835    let paginated_response = server.get_json::<api::SatInscriptions>("/r/sat/5000000000/1");
5836
5837    assert_eq!(paginated_response.ids.len(), 11);
5838    assert!(!paginated_response.more);
5839    assert_eq!(paginated_response.page, 1);
5840
5841    assert_eq!(
5842      server
5843        .get_json::<api::SatInscription>("/r/sat/5000000000/at/0")
5844        .id,
5845      Some(ids[0])
5846    );
5847
5848    assert_eq!(
5849      server
5850        .get_json::<api::SatInscription>("/r/sat/5000000000/at/-111")
5851        .id,
5852      Some(ids[0])
5853    );
5854
5855    assert_eq!(
5856      server
5857        .get_json::<api::SatInscription>("/r/sat/5000000000/at/110")
5858        .id,
5859      Some(ids[110])
5860    );
5861
5862    assert_eq!(
5863      server
5864        .get_json::<api::SatInscription>("/r/sat/5000000000/at/-1")
5865        .id,
5866      Some(ids[110])
5867    );
5868
5869    assert!(server
5870      .get_json::<api::SatInscription>("/r/sat/5000000000/at/111")
5871      .id
5872      .is_none());
5873  }
5874
5875  #[test]
5876  fn children_recursive_endpoint() {
5877    let server = TestServer::builder().chain(Chain::Regtest).build();
5878    server.mine_blocks(1);
5879
5880    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
5881      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
5882      ..default()
5883    });
5884
5885    let parent_inscription_id = InscriptionId {
5886      txid: parent_txid,
5887      index: 0,
5888    };
5889
5890    server.assert_response(
5891      format!("/r/children/{parent_inscription_id}"),
5892      StatusCode::NOT_FOUND,
5893      &format!("inscription {parent_inscription_id} not found"),
5894    );
5895
5896    server.mine_blocks(1);
5897
5898    let children_json =
5899      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}"));
5900    assert_eq!(children_json.ids.len(), 0);
5901
5902    let mut builder = script::Builder::new();
5903    for _ in 0..111 {
5904      builder = Inscription {
5905        content_type: Some("text/plain".into()),
5906        body: Some("hello".into()),
5907        parents: vec![parent_inscription_id.value()],
5908        unrecognized_even_field: false,
5909        ..default()
5910      }
5911      .append_reveal_script_to_builder(builder);
5912    }
5913
5914    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);
5915
5916    let txid = server.core.broadcast_tx(TransactionTemplate {
5917      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
5918      ..default()
5919    });
5920
5921    server.mine_blocks(1);
5922
5923    let first_child_inscription_id = InscriptionId { txid, index: 0 };
5924    let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
5925    let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
5926    let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };
5927
5928    let children_json =
5929      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}"));
5930
5931    assert_eq!(children_json.ids.len(), 100);
5932    assert_eq!(children_json.ids[0], first_child_inscription_id);
5933    assert_eq!(children_json.ids[99], hundredth_child_inscription_id);
5934    assert!(children_json.more);
5935    assert_eq!(children_json.page, 0);
5936
5937    let children_json =
5938      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}/1"));
5939
5940    assert_eq!(children_json.ids.len(), 11);
5941    assert_eq!(children_json.ids[0], hundred_first_child_inscription_id);
5942    assert_eq!(children_json.ids[10], hundred_eleventh_child_inscription_id);
5943    assert!(!children_json.more);
5944    assert_eq!(children_json.page, 1);
5945  }
5946
5947  #[test]
5948  fn inscriptions_in_block_page() {
5949    let server = TestServer::builder()
5950      .chain(Chain::Regtest)
5951      .index_sats()
5952      .build();
5953
5954    for _ in 0..101 {
5955      server.mine_blocks(1);
5956    }
5957
5958    for i in 0..101 {
5959      server.core.broadcast_tx(TransactionTemplate {
5960        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
5961        ..default()
5962      });
5963    }
5964
5965    server.mine_blocks(1);
5966
5967    server.assert_response_regex(
5968      "/inscriptions/block/102",
5969      StatusCode::OK,
5970      r".*(<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>.*){100}.*",
5971    );
5972
5973    server.assert_response_regex(
5974      "/inscriptions/block/102/1",
5975      StatusCode::OK,
5976      r".*<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>.*",
5977    );
5978  }
5979
5980  #[test]
5981  fn inscription_query_display() {
5982    assert_eq!(
5983      query::Inscription::Id(inscription_id(1)).to_string(),
5984      "1111111111111111111111111111111111111111111111111111111111111111i1"
5985    );
5986    assert_eq!(query::Inscription::Number(1).to_string(), "1")
5987  }
5988
5989  #[test]
5990  fn inscription_not_found() {
5991    TestServer::builder()
5992      .chain(Chain::Regtest)
5993      .build()
5994      .assert_response(
5995        "/inscription/0",
5996        StatusCode::NOT_FOUND,
5997        "inscription 0 not found",
5998      );
5999  }
6000
6001  #[test]
6002  fn looking_up_inscription_by_sat_requires_sat_index() {
6003    TestServer::builder()
6004      .chain(Chain::Regtest)
6005      .build()
6006      .assert_response(
6007        "/inscription/abcd",
6008        StatusCode::NOT_FOUND,
6009        "sat index required",
6010      );
6011  }
6012
6013  #[test]
6014  fn delegate() {
6015    let server = TestServer::builder().chain(Chain::Regtest).build();
6016
6017    server.mine_blocks(1);
6018
6019    let delegate = Inscription {
6020      content_type: Some("text/html".into()),
6021      body: Some("foo".into()),
6022      ..default()
6023    };
6024
6025    let txid = server.core.broadcast_tx(TransactionTemplate {
6026      inputs: &[(1, 0, 0, delegate.to_witness())],
6027      ..default()
6028    });
6029
6030    let delegate = InscriptionId { txid, index: 0 };
6031
6032    server.mine_blocks(1);
6033
6034    let inscription = Inscription {
6035      delegate: Some(delegate.value()),
6036      ..default()
6037    };
6038
6039    let txid = server.core.broadcast_tx(TransactionTemplate {
6040      inputs: &[(2, 0, 0, inscription.to_witness())],
6041      ..default()
6042    });
6043
6044    server.mine_blocks(1);
6045
6046    let id = InscriptionId { txid, index: 0 };
6047
6048    server.assert_response_regex(
6049      format!("/inscription/{id}"),
6050      StatusCode::OK,
6051      format!(
6052        ".*<h1>Inscription 1</h1>.*
6053        <dl>
6054          <dt>id</dt>
6055          <dd class=monospace>{id}</dd>
6056          .*
6057          <dt>delegate</dt>
6058          <dd><a href=/inscription/{delegate}>{delegate}</a></dd>
6059          .*
6060        </dl>.*"
6061      )
6062      .unindent(),
6063    );
6064
6065    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
6066
6067    server.assert_response(format!("/preview/{id}"), StatusCode::OK, "foo");
6068  }
6069
6070  #[test]
6071  fn proxy() {
6072    let server = TestServer::builder().chain(Chain::Regtest).build();
6073
6074    server.mine_blocks(1);
6075
6076    let inscription = Inscription {
6077      content_type: Some("text/html".into()),
6078      body: Some("foo".into()),
6079      ..default()
6080    };
6081
6082    let txid = server.core.broadcast_tx(TransactionTemplate {
6083      inputs: &[(1, 0, 0, inscription.to_witness())],
6084      ..default()
6085    });
6086
6087    server.mine_blocks(1);
6088
6089    let id = InscriptionId { txid, index: 0 };
6090
6091    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
6092
6093    let server_with_proxy = TestServer::builder()
6094      .chain(Chain::Regtest)
6095      .server_option("--content-proxy", server.url.as_ref())
6096      .build();
6097
6098    server_with_proxy.mine_blocks(1);
6099
6100    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
6101    server_with_proxy.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
6102  }
6103
6104  #[test]
6105  fn block_info() {
6106    let server = TestServer::new();
6107
6108    pretty_assert_eq!(
6109      server.get_json::<api::BlockInfo>("/r/blockinfo/0"),
6110      api::BlockInfo {
6111        average_fee: 0,
6112        average_fee_rate: 0,
6113        bits: 486604799,
6114        chainwork: [0; 32],
6115        confirmations: 0,
6116        difficulty: 0.0,
6117        hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
6118          .parse()
6119          .unwrap(),
6120        height: 0,
6121        max_fee: 0,
6122        max_fee_rate: 0,
6123        max_tx_size: 0,
6124        median_fee: 0,
6125        median_time: None,
6126        merkle_root: TxMerkleNode::all_zeros(),
6127        min_fee: 0,
6128        min_fee_rate: 0,
6129        next_block: None,
6130        nonce: 0,
6131        previous_block: None,
6132        subsidy: 0,
6133        target: "00000000ffff0000000000000000000000000000000000000000000000000000"
6134          .parse()
6135          .unwrap(),
6136        timestamp: 0,
6137        total_fee: 0,
6138        total_size: 0,
6139        total_weight: 0,
6140        transaction_count: 0,
6141        version: 1,
6142      },
6143    );
6144
6145    server.mine_blocks(1);
6146
6147    pretty_assert_eq!(
6148      server.get_json::<api::BlockInfo>("/r/blockinfo/1"),
6149      api::BlockInfo {
6150        average_fee: 0,
6151        average_fee_rate: 0,
6152        bits: 0,
6153        chainwork: [0; 32],
6154        confirmations: 0,
6155        difficulty: 0.0,
6156        hash: "56d05060a0280d0712d113f25321158747310ece87ea9e299bde06cf385b8d85"
6157          .parse()
6158          .unwrap(),
6159        height: 1,
6160        max_fee: 0,
6161        max_fee_rate: 0,
6162        max_tx_size: 0,
6163        median_fee: 0,
6164        median_time: None,
6165        merkle_root: TxMerkleNode::all_zeros(),
6166        min_fee: 0,
6167        min_fee_rate: 0,
6168        next_block: None,
6169        nonce: 0,
6170        previous_block: None,
6171        subsidy: 0,
6172        target: BlockHash::all_zeros(),
6173        timestamp: 0,
6174        total_fee: 0,
6175        total_size: 0,
6176        total_weight: 0,
6177        transaction_count: 0,
6178        version: 1,
6179      },
6180    )
6181  }
6182
6183  #[test]
6184  fn authentication_requires_username_and_password() {
6185    assert!(Arguments::try_parse_from(["ord", "--server-username", "server", "foo"]).is_err());
6186    assert!(Arguments::try_parse_from(["ord", "--server-password", "server", "bar"]).is_err());
6187    assert!(Arguments::try_parse_from([
6188      "ord",
6189      "--server-username",
6190      "foo",
6191      "--server-password",
6192      "bar",
6193      "server"
6194    ])
6195    .is_ok());
6196  }
6197
6198  #[test]
6199  fn inscriptions_can_be_hidden_with_config() {
6200    let core = mockcore::builder()
6201      .network(Chain::Regtest.network())
6202      .build();
6203
6204    core.mine_blocks(1);
6205
6206    let txid = core.broadcast_tx(TransactionTemplate {
6207      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
6208      ..default()
6209    });
6210
6211    core.mine_blocks(1);
6212
6213    let inscription = InscriptionId { txid, index: 0 };
6214
6215    let server = TestServer::builder()
6216      .core(core)
6217      .config(&format!("hidden: [{inscription}]"))
6218      .build();
6219
6220    server.assert_response_regex(format!("/inscription/{inscription}"), StatusCode::OK, ".*");
6221
6222    server.assert_response_regex(
6223      format!("/content/{inscription}"),
6224      StatusCode::OK,
6225      PreviewUnknownHtml.to_string(),
6226    );
6227  }
6228
6229  #[test]
6230  fn update_endpoint_is_not_available_when_not_in_integration_test_mode() {
6231    let server = TestServer::builder().build();
6232    server.assert_response("/update", StatusCode::NOT_FOUND, "");
6233  }
6234}