diff --git a/Cargo.lock b/Cargo.lock index af8c453..e68527b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3902,6 +3902,7 @@ dependencies = [ "nostr", "nwc", "portal", + "portal-macros", "portal-rates", "portal-sdk", "portal-wallet", diff --git a/crates/portal-rest/Cargo.toml b/crates/portal-rest/Cargo.toml index 5d86f44..7761298 100644 --- a/crates/portal-rest/Cargo.toml +++ b/crates/portal-rest/Cargo.toml @@ -12,6 +12,7 @@ portal-wallet = { path = "../portal-wallet" } portal-sdk = { path = "../portal-sdk" } portal = { version = "0.1.0", path = "../portal" } portal-rates = { path = "../portal-rates" } +portal-macros = { path = "../portal-macros" } nostr = { workspace = true } diff --git a/crates/portal-rest/README.md b/crates/portal-rest/README.md index e388f85..c5d495d 100644 --- a/crates/portal-rest/README.md +++ b/crates/portal-rest/README.md @@ -17,7 +17,7 @@ docker run -d -p 3000:3000 \ getportal/sdk-daemon:latest ``` -Use `ws://localhost:3000/ws` as the SDK `serverUrl` and your token in `client.authenticate(...)`. Check: `curl http://localhost:3000/health` → `OK`. +Use `ws://localhost:3000/ws` as the SDK `serverUrl` and your token in `client.authenticate(...)`. Check: `curl http://localhost:3000/health` → `OK`, `curl http://localhost:3000/version` → `{"version":"0.1.0","git_commit":"a1b2c3d4"}`. **From source:** diff --git a/crates/portal-rest/src/main.rs b/crates/portal-rest/src/main.rs index 6881be9..b994e6c 100644 --- a/crates/portal-rest/src/main.rs +++ b/crates/portal-rest/src/main.rs @@ -28,6 +28,12 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use portal_wallet::{BreezSparkWallet, NwcWallet, PortalWallet}; +use portal_macros::fetch_git_hash; + +/// Build-time version from Cargo.toml (used for Docker image tagging and runtime /version endpoint). +pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// Git commit hash at build time (from portal_macros::fetch_git_hash! or PORTAL_GIT_HASH env). +const GIT_COMMIT: &str = fetch_git_hash!(); #[derive(Debug, thiserror::Error)] enum ApiError { @@ -107,6 +113,19 @@ async fn health_check() -> &'static str { "OK" } +#[derive(Serialize)] +struct VersionResponse { + version: &'static str, + git_commit: &'static str, +} + +async fn version() -> Json { + Json(VersionResponse { + version: APP_VERSION, + git_commit: GIT_COMMIT, + }) +} + async fn handle_ws_upgrade( ws: WebSocketUpgrade, State(state): State, @@ -127,7 +146,12 @@ async fn main() -> anyhow::Result<()> { .init(); let config = config::Settings::load()?; - info!("Config loaded: {:?}", config); + info!( + listen_port = config.info.listen_port, + wallet = format!("{:?}", config.wallet.ln_backend).to_lowercase(), + relays = config.nostr.relays.join(","), + "Config loaded", + ); // Settings validation config.validate()?; @@ -164,14 +188,21 @@ async fn main() -> anyhow::Result<()> { market_api: portal_rates::MarketAPI::new().expect("Failed to create market API"), }; - // Create router with middleware - let app = Router::new() + // Public routes (no auth): health and version for Docker/orchestrators and support. + let public = Router::new() .route("/health", get(health_check)) + .route("/version", get(version)); + + let ws = Router::new() .route("/ws", get(handle_ws_upgrade)) .layer(middleware::from_fn_with_state( state.clone(), auth_middleware, - )) + )); + + let app = Router::new() + .merge(public) + .merge(ws) .layer( CorsLayer::new() .allow_origin(Any) diff --git a/docs/getting-started/docker-deployment.md b/docs/getting-started/docker-deployment.md index 0be9608..909afb4 100644 --- a/docs/getting-started/docker-deployment.md +++ b/docs/getting-started/docker-deployment.md @@ -2,6 +2,24 @@ Deploy the Portal SDK Daemon using Docker for easy setup and management. +## Versioning + +The daemon exposes its **application version** (from `portal-rest`’s Cargo.toml) for Docker and support: + +- **`GET /version`** (no auth) — returns JSON: `{"version":"0.1.0","git_commit":"a1b2c3d4"}` (or `"unknown"` when not built from git). Use this to verify which image/version is running (e.g. `curl http://localhost:3000/version`). +- **API paths** — the WebSocket API is at **`/ws`** (auth required). + +For **Docker Hub**, tag images with the crate version so users can pin to a release: + +```bash +# Build and tag with version (e.g. from crates/portal-rest/Cargo.toml) +docker build -t youruser/sdk-daemon:0.1.0 -t youruser/sdk-daemon:latest . +docker push youruser/sdk-daemon:0.1.0 +docker push youruser/sdk-daemon:latest +``` + +Use `:0.1.0` (or the current version) for reproducible deployments; use `:latest` for convenience. + ## Quick Deployment ### Using Pre-built Image diff --git a/flake.nix b/flake.nix index deae689..20305cf 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,15 @@ rustc = rustToolchain; }; + # Static build: same Rust 1.90 as overlay, musl target (lnurl-models needs rustc 1.88+) + rustToolchainStatic = pkgs.rust-bin.stable."1.90.0".default.override { + targets = [ "x86_64-unknown-linux-musl" ]; + }; + staticRustPlatform = pkgs.makeRustPlatform { + cargo = rustToolchainStatic; + rustc = rustToolchainStatic; + }; + rest' = platform: platform.buildRustPackage { pname = "portal-rest"; version = (pkgs.lib.importTOML ./crates/portal-rest/Cargo.toml).package.version; @@ -92,7 +101,27 @@ inherit backend; rest = rest' rustPlatform; - rest-static = rest' pkgs.pkgsStatic.rustPlatform; + # Static binary for Docker: overlay Rust 1.90 + musl stdenv + static openssl + rest-static = staticRustPlatform.buildRustPackage { + pname = "portal-rest"; + version = (pkgs.lib.importTOML ./crates/portal-rest/Cargo.toml).package.version; + src = pkgs.lib.sources.sourceFilesBySuffices ./. [ ".rs" "Cargo.toml" "Cargo.lock" "fiatUnits.json" "example.config.toml" ]; + cargoHash = ""; + cargoLock = { + lockFile = ./Cargo.lock; + outputHashes = { + "cashu-0.11.0" = "sha256-kwwfQX5vDclfa86xPbbkbu+bh1VQXlX+imunUUoTYV4="; + "nostr-0.43.0" = "sha256-1TLthpGnDLUmnBoq2CneWnfTMwRocitbD4+wnrlCA44="; + "breez-sdk-common-0.1.0" = "sha256-b8R4V8L7lM0AOy9NxhiIt+RsIBHJdQPpfw9SN1/P//E="; + }; + }; + buildAndTestSubdir = "crates/portal-rest"; + doCheck = false; + stdenv = pkgs.pkgsStatic.stdenv; + nativeBuildInputs = with pkgs; [ protobuf pkg-config ]; + buildInputs = with pkgs.pkgsStatic; [ openssl ]; + meta.mainProgram = "rest"; + }; rest-docker = let minimal-closure = pkgs.runCommand "minimal-rust-app" { @@ -102,7 +131,7 @@ cp ${rest-static}/bin/rest $out/bin/ for binary in $out/bin/*; do - remove-references-to -t ${pkgs.pkgsStatic.rustPlatform.rust.rustc} "$binary" + remove-references-to -t ${rustToolchainStatic} "$binary" done ''; in pkgs.dockerTools.buildLayeredImage { @@ -114,9 +143,10 @@ ExposedPorts = { "3000/tcp" = {}; }; + # Only non-secret defaults. Required (AUTH_TOKEN, NOSTR__PRIVATE_KEY) and + # optional env are passed when starting the container (docker run -e / --env-file). Env = [ - "AUTH_TOKEN=remeber-to-change-this" - "NOSTR_RELAYS=wss://relay.getportal.cc,wss://relay.damus.io,wss://relay.nostr.net" + "PORTAL__INFO__LISTEN_PORT=3000" "RUST_LOG=portal=debug,rest=debug,info" ]; };