Skip to main content

miden_node_ntx_builder/
server.rs

1use anyhow::Context;
2use miden_node_proto::generated::note::NoteId;
3use miden_node_proto::generated::ntx_builder::{self, api_server};
4use miden_node_proto_build::ntx_builder_api_descriptor;
5use miden_node_utils::panic::{CatchPanicLayer, catch_panic_layer_fn};
6use miden_node_utils::tracing::grpc::grpc_trace_fn;
7use miden_protocol::Word;
8use tokio::net::TcpListener;
9use tokio_stream::wrappers::TcpListenerStream;
10use tonic::{Request, Response, Status};
11use tonic_reflection::server;
12use tower_http::trace::TraceLayer;
13
14use crate::COMPONENT;
15use crate::db::Db;
16
17// NTX BUILDER RPC SERVER
18// ================================================================================================
19
20/// gRPC server for the network transaction builder.
21///
22/// Exposes endpoints for querying note execution errors, useful for debugging
23/// network notes that fail to be consumed.
24pub struct NtxBuilderRpcServer {
25    db: Db,
26}
27
28impl NtxBuilderRpcServer {
29    pub fn new(db: Db) -> Self {
30        Self { db }
31    }
32
33    /// Starts the gRPC server on the given listener.
34    pub async fn serve(self, listener: TcpListener) -> anyhow::Result<()> {
35        let api_service = api_server::ApiServer::new(self);
36        let reflection_service = server::Builder::configure()
37            .register_file_descriptor_set(ntx_builder_api_descriptor())
38            .build_v1()
39            .context("failed to build reflection service")?;
40
41        tracing::info!(
42            target: COMPONENT,
43            endpoint = ?listener.local_addr(),
44            "NTX builder gRPC server initialized",
45        );
46
47        tonic::transport::Server::builder()
48            .layer(CatchPanicLayer::custom(catch_panic_layer_fn))
49            .layer(TraceLayer::new_for_grpc().make_span_with(grpc_trace_fn))
50            .add_service(api_service)
51            .add_service(reflection_service)
52            .serve_with_incoming(TcpListenerStream::new(listener))
53            .await
54            .context("failed to serve NTX builder gRPC API")
55    }
56}
57
58#[tonic::async_trait]
59impl api_server::Api for NtxBuilderRpcServer {
60    #[expect(clippy::cast_sign_loss)]
61    async fn get_note_error(
62        &self,
63        request: Request<NoteId>,
64    ) -> Result<Response<ntx_builder::GetNoteErrorResponse>, Status> {
65        let note_id_proto = request.into_inner();
66
67        let note_id_digest: Word = note_id_proto
68            .id
69            .as_ref()
70            .ok_or_else(|| Status::invalid_argument("missing note ID digest"))?
71            .try_into()
72            .map_err(|_| Status::invalid_argument("invalid note ID digest"))?;
73
74        let note_id = miden_protocol::note::NoteId::from_raw(note_id_digest);
75
76        let row = self.db.get_note_error(note_id).await.map_err(|err| {
77            tracing::error!(err = %err, "failed to query note error from DB");
78            Status::internal("database error")
79        })?;
80
81        let Some(row) = row else {
82            return Err(Status::not_found("note not found in ntx-builder database"));
83        };
84
85        let response = ntx_builder::GetNoteErrorResponse {
86            error: row.last_error,
87            attempt_count: row.attempt_count as u32,
88            last_attempt_block_num: row.last_attempt.map(|v| v as u32),
89        };
90
91        Ok(Response::new(response))
92    }
93}