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