nft_server/
server.rs

1use std::{net::SocketAddr, sync::Arc};
2
3use axum::{
4    body::Body,
5    extract::{Path, State},
6    http::StatusCode,
7    response::{IntoResponse, Response},
8    routing::get,
9    Json, Router,
10};
11use ethers::types::U256;
12use tokio::task::JoinHandle;
13use tracing::{info_span, Instrument, Span};
14
15use crate::MetadataGenerator;
16
17/// Simple handler that consults the metadata generator, and returns EITHER the
18/// token metadata, or a 404
19pub async fn nft_handler<T>(
20    Path(token_id): Path<String>,
21    State(generator): State<Arc<T>>,
22) -> Response
23where
24    T: MetadataGenerator,
25{
26    let e_500 = (
27        StatusCode::INTERNAL_SERVER_ERROR,
28        "service temporarily unavailable",
29    );
30    let token_id = match U256::from_dec_str(&token_id) {
31        Ok(id) => id,
32        Err(e) => {
33            tracing::error!(token_id = ?token_id, error = %e, "error in token_id parsing");
34            return e_500.into_response();
35        }
36    };
37
38    match generator.metadata_for(token_id).await {
39        Ok(Some(metadata)) => (
40            [("Cache-Control", "max-age=300, must-revalidate")],
41            Json(metadata),
42        )
43            .into_response(),
44        Ok(None) => (
45            StatusCode::NOT_FOUND,
46            [("Cache-Control", "max-age=300, must-revalidate")],
47            "unknown token id",
48        )
49            .into_response(),
50        Err(e) => {
51            tracing::error!(error = %e, "error in metadata lookup");
52            e_500.into_response()
53        }
54    }
55}
56
57/// Simple handler that consults the metadata generator, and returns EITHER the
58/// contract metadata, or a 404
59pub async fn contract_handler<T>(State(generator): State<Arc<T>>) -> Response
60where
61    T: MetadataGenerator,
62{
63    match generator.contract_metadata().await {
64        Some(metadata) => (
65            [("Cache-Control", "max-age=300, must-revalidate")],
66            Json(metadata),
67        )
68            .into_response(),
69        None => (StatusCode::NOT_FOUND, "no contract metadata").into_response(),
70    }
71}
72
73/// Fallback handler that returns a 404 with body `"unknown route"`
74pub async fn return_404() -> impl IntoResponse {
75    (StatusCode::NOT_FOUND, "unknown route")
76}
77
78/// Handler for healthcheck
79pub async fn return_200() -> impl IntoResponse {
80    StatusCode::OK
81}
82
83/// Serve an NFT generator at the specified socket address, running in a
84/// provided span.
85///
86/// Adds routes for `/:token_id` and `/`, as well as a fallback 404. This is a
87/// simple, works-out-of-the-box JSON metadata server with no additional app or
88/// routing customization. If you would like to add additional routes, consider
89/// defining the axum `Router` and handlers separately, and passing your router
90/// to `serve_router`
91pub fn serve_generator_with_span<T>(
92    t: T,
93    socket: impl Into<SocketAddr>,
94    span: Span,
95) -> JoinHandle<()>
96where
97    T: MetadataGenerator + Send + Sync + 'static,
98{
99    let app = Router::<_, Body>::with_state(Arc::new(t))
100        .route("/healthcheck", get(return_200))
101        .route(
102            "/favicon.ico",
103            get(|| async move { (StatusCode::NOT_FOUND, "") }),
104        )
105        .route("/:token_id", get(nft_handler))
106        .route("/", get(contract_handler))
107        .fallback(return_404);
108
109    serve_router_with_span(app, socket, span)
110}
111
112/// Serve an NFT generator at the specified socket address.
113///
114/// Adds routes for `/:token_id` and `/`, as well as a fallback 404. This is a
115/// simple, works-out-of-the-box JSON metadata server with no additional app or
116/// routing customization. If you would like to add additional routes, consider
117/// defining the axum `Router` and handlers separately, and passing your router
118/// to `serve_router`
119pub fn serve_generator<T>(t: T, socket: impl Into<SocketAddr>) -> JoinHandle<()>
120where
121    T: MetadataGenerator + Send + Sync + 'static,
122{
123    let span = info_span!("serve_generator");
124    serve_generator_with_span(t, socket, span)
125}
126
127/// Serve an app with some shared state at the specified socket address
128/// instrumented with the provided span.
129///
130/// Intended to allow full customization of the router. If a simple
131/// no-customization JSON metadata server is required, instead use
132pub fn serve_router_with_span<T>(
133    app: Router<Arc<T>>,
134    socket: impl Into<SocketAddr>,
135    span: Span,
136) -> JoinHandle<()>
137where
138    T: MetadataGenerator + Send + Sync + 'static,
139{
140    let addr = socket.into();
141    tokio::spawn(async move {
142        Instrument::instrument(
143            axum::Server::bind(&addr).serve(app.into_make_service()),
144            span,
145        )
146        .await
147        .unwrap();
148    })
149}
150
151/// Serve an app with some shared state at the specified socket address.
152/// Intended to allow full customization of the router. If a simple
153/// no-customization JSON metadata server is required, instead use
154/// [`serve_generator`].
155pub fn serve_router<T>(app: Router<Arc<T>>, socket: impl Into<SocketAddr>) -> JoinHandle<()>
156where
157    T: MetadataGenerator + Send + Sync + 'static,
158{
159    let span = info_span!("serve_router");
160    serve_router_with_span(app, socket, span)
161}