Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ target
.git
.env
.DS_Store
riskmesh-b0fe9-firebase-adminsdk-fbsvc-46e7e0876e.json
solana/id.json
9 changes: 6 additions & 3 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ SWITCHBOARD_QUEUE=EYiAmGSdsQTuCw413V5BzaruWuCCSDgTPtBGvLkXHbe7
# 스케줄러 실행 간격 (cron 표현식, 6필드: sec min hour dom month dow)
# 기본값: 15분마다
ORACLE_CHECK_CRON="0 */15 * * * *"
FIREBASE_SYNC_CRON="0/30 * * * * *"

# Firebase / Firestore (Admin / service account)
FIREBASE_PROJECT_ID=riskmesh-b0fe9
FIREBASE_DATABASE=(default)
FIREBASE_TEST_COLLECTION=riskmesh_test
# One of these is required:
FIREBASE_SERVICE_ACCOUNT_PATH=
FIREBASE_SERVICE_ACCOUNT_JSON=
FIREBASE_MASTER_POLICIES_COLLECTION=master_policies
FIREBASE_FLIGHT_POLICIES_COLLECTION=flight_policies
FIREBASE_SYNC_METADATA_COLLECTION=sync_metadata
# Required: fixed service account file name
FIREBASE_SERVICE_ACCOUNT_PATH={file path}/riskmesh-b0fe9-firebase-adminsdk-fbsvc-46e7e0876e.json
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/target/
.env
riskmesh-b0fe9-firebase-adminsdk-fbsvc-46e7e0876e.json
14 changes: 10 additions & 4 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:1.78-bookworm AS builder
FROM rust:1.88-bookworm AS builder

WORKDIR /app

Expand All @@ -9,21 +9,27 @@ RUN apt-get update \
COPY Cargo.toml Cargo.lock ./
COPY src ./src

RUN cargo build --release --bin oracle-daemon
RUN cargo build --release --locked --bin oracle-daemon

FROM debian:bookworm-slim AS runtime

WORKDIR /app

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates libssl3 \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --create-home --home-dir /app appuser \
&& mkdir -p /run/secrets/firebase /run/secrets/solana \
&& chown -R appuser:appuser /app /run/secrets

COPY --from=builder /app/target/release/oracle-daemon /usr/local/bin/oracle-daemon

ENV RUST_LOG=info
ENV WEB_BIND_ADDR=0.0.0.0:3000
ENV LEADER_KEYPAIR_PATH=/app/secrets/id.json
ENV LEADER_KEYPAIR_PATH=/run/secrets/solana/id.json
ENV FIREBASE_SERVICE_ACCOUNT_PATH=/run/secrets/firebase/service-account.json

USER appuser

EXPOSE 3000

Expand Down
20 changes: 16 additions & 4 deletions backend/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod client;
mod error;
mod handlers;
mod repository;
pub(crate) mod repository;
mod router;
mod service;
mod state;
Expand All @@ -10,16 +10,28 @@ mod types;
use std::{net::SocketAddr, sync::Arc};

use anyhow::{Context, Result};
use tower_http::cors::{Any, CorsLayer};

use crate::config::Config;
use crate::{config::Config, events::EventBus};

pub async fn start(config: Arc<Config>) -> Result<()> {
pub async fn start(config: Arc<Config>, event_bus: Arc<EventBus>) -> Result<()> {
let addr: SocketAddr = config
.web_bind_addr
.parse()
.with_context(|| format!("WEB_BIND_ADDR 파싱 실패: {}", config.web_bind_addr))?;

let app = router::build_router(state::AppState { config });
let firebase_repository = Arc::new(repository::FirebaseRepository::from_env()?);
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);

let app = router::build_router(state::AppState {
config,
firebase_repository,
event_bus,
})
.layer(cors);

tracing::info!("[api] listening on http://{addr}");

Expand Down
23 changes: 21 additions & 2 deletions backend/src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,31 @@ pub(super) struct ApiError(pub anyhow::Error);

impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let message = self.0.to_string();
let status = if is_not_found_error(&message) {
StatusCode::NOT_FOUND
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
let error_message = if status == StatusCode::NOT_FOUND {
"account not found".to_string()
} else {
message
};

(
StatusCode::INTERNAL_SERVER_ERROR,
status,
Json(serde_json::json!({
"error": self.0.to_string(),
"error": error_message,
})),
)
.into_response()
}
}

fn is_not_found_error(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
lower.contains("accountnotfound")
|| lower.contains("account not found")
|| lower.contains("could not find account")
}
63 changes: 40 additions & 23 deletions backend/src/api/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
use axum::{
extract::{Path, State},
extract::{Path, Query, State},
response::sse::{Event, Sse},
Json,
};
use futures_util::Stream;
use std::convert::Infallible;

use crate::solana::client::SolanaClient;
use solana_sdk::pubkey::Pubkey;

use super::{
error::ApiError,
service,
state::AppState,
types::{
CreateFlightPolicyRequest, CreateFlightPolicyResponse, FlightPoliciesResponse,
FlightPolicyResponse, FirebaseTestDocumentResponse,
HealthResponse, MasterFlightPoliciesResponse, MasterPoliciesResponse,
MasterPoliciesTreeResponse, MasterPolicyAccountsResponse, MasterPolicyResponse,
CreateFlightPolicyRequest, CreateFlightPolicyResponse, EventsQuery,
FirebaseTestDocumentResponse, FlightPoliciesQuery, FlightPoliciesResponse, HealthResponse,
MasterFlightPoliciesResponse, MasterPoliciesQuery, MasterPoliciesResponse,
MasterPoliciesTreeResponse, MasterPolicyAccountsResponse,
},
};

Expand All @@ -23,9 +27,10 @@ pub(super) async fn health(State(state): State<AppState>) -> Json<HealthResponse

pub(super) async fn get_master_policies(
State(state): State<AppState>,
Query(query): Query<MasterPoliciesQuery>,
) -> Result<Json<MasterPoliciesResponse>, ApiError> {
let client = SolanaClient::new(&state.config.rpc_url);
service::list_master_policies(&client, &state.config)
service::list_master_policies(&state.firebase_repository, &query)
.await
.map(Json)
.map_err(ApiError)
}
Expand All @@ -42,13 +47,13 @@ pub(super) async fn get_master_policy_accounts(
pub(super) async fn get_master_policy(
State(state): State<AppState>,
Path(master_policy_pubkey): Path<String>,
) -> Result<Json<MasterPolicyResponse>, ApiError> {
let client = SolanaClient::new(&state.config.rpc_url);
let master_policy_pubkey = master_policy_pubkey
.parse()
) -> Result<Json<crate::oracle::program_accounts::MasterPolicyInfo>, ApiError> {
master_policy_pubkey
.parse::<Pubkey>()
.map_err(|e| ApiError(anyhow::anyhow!("master_policy_pubkey 주소 파싱 실패: {e}")))?;

service::get_master_policy(&client, &state.config, &master_policy_pubkey)
service::get_master_policy(&state.firebase_repository, &master_policy_pubkey)
.await
.map(Json)
.map_err(ApiError)
}
Expand All @@ -62,34 +67,42 @@ pub(super) async fn post_firebase_test_document(
.map_err(ApiError)
}

pub(super) async fn get_events(
State(state): State<AppState>,
Query(query): Query<EventsQuery>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
service::stream_events(state.event_bus, query)
}

pub(super) async fn get_flight_policies(
State(state): State<AppState>,
Query(query): Query<FlightPoliciesQuery>,
) -> Result<Json<FlightPoliciesResponse>, ApiError> {
let client = SolanaClient::new(&state.config.rpc_url);
service::list_flight_policies(&client, &state.config)
service::list_flight_policies(&state.firebase_repository, &query)
.await
.map(Json)
.map_err(ApiError)
}

pub(super) async fn get_flight_policy(
State(state): State<AppState>,
Path(flight_policy_pubkey): Path<String>,
) -> Result<Json<FlightPolicyResponse>, ApiError> {
let client = SolanaClient::new(&state.config.rpc_url);
let flight_policy_pubkey = flight_policy_pubkey
.parse()
) -> Result<Json<crate::oracle::program_accounts::FlightPolicyInfo>, ApiError> {
flight_policy_pubkey
.parse::<Pubkey>()
.map_err(|e| ApiError(anyhow::anyhow!("flight_policy_pubkey 주소 파싱 실패: {e}")))?;

service::get_flight_policy(&client, &state.config, &flight_policy_pubkey)
service::get_flight_policy(&state.firebase_repository, &flight_policy_pubkey)
.await
.map(Json)
.map_err(ApiError)
}

pub(super) async fn get_master_policies_tree(
State(state): State<AppState>,
) -> Result<Json<MasterPoliciesTreeResponse>, ApiError> {
let client = SolanaClient::new(&state.config.rpc_url);
service::list_master_policies_tree(&client, &state.config)
service::list_master_policies_tree(&state.firebase_repository, &state.config)
.await
.map(Json)
.map_err(ApiError)
}
Expand All @@ -98,12 +111,16 @@ pub(super) async fn get_flight_policies_by_master(
State(state): State<AppState>,
Path(master_policy_pubkey): Path<String>,
) -> Result<Json<MasterFlightPoliciesResponse>, ApiError> {
let client = SolanaClient::new(&state.config.rpc_url);
let master_policy_pubkey = master_policy_pubkey
.parse()
.map_err(|e| ApiError(anyhow::anyhow!("master_policy_pubkey 주소 파싱 실패: {e}")))?;

service::list_flight_policies_by_master(&client, &state.config, &master_policy_pubkey)
service::list_flight_policies_by_master(
&state.firebase_repository,
&state.config,
&master_policy_pubkey,
)
.await
.map(Json)
.map_err(ApiError)
}
Expand Down
Loading
Loading