Skip to main content

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