Skip to content

Commit

Permalink
Merge pull request #34 from fedimint/stateless-clientd
Browse files Browse the repository at this point in the history
  • Loading branch information
Kodylow authored Apr 27, 2024
2 parents bc043e1 + 4bf43c0 commit 20615bd
Show file tree
Hide file tree
Showing 58 changed files with 638 additions and 213 deletions.
41 changes: 41 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["multimint", "fedimint-clientd"]
members = ["multimint", "fedimint-clientd", "clientd-stateless"]
resolver = "2"
version = "0.3.3"

Expand All @@ -15,7 +15,13 @@ ci = ["github"]
# The installers to generate for each app
installers = ["shell"]
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
targets = [
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"x86_64-pc-windows-msvc",
]
# Publish jobs to run in CI
pr-run-mode = "plan"
# Whether to install an updater program
Expand Down
49 changes: 49 additions & 0 deletions clientd-stateless/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[package]
name = "clientd-statless"
version = "0.3.3"
edition = "2021"
description = "A stateless fedimint client daemon"
repository = "https://github.com/fedimint/fedimint-clientd"
keywords = ["fedimint", "bitcoin", "lightning", "ecash"]
license = "MIT"

[dependencies]
anyhow = "1.0.75"
axum = { version = "0.7.1", features = ["json", "ws"] }
axum-macros = "0.4.0"
dotenv = "0.15.0"
fedimint = "0.0.1"
serde = "1.0.193"
serde_json = "1.0.108"
tokio = { version = "1.34.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
fedimint-client = "0.3.0"
fedimint-core = "0.3.0"
fedimint-wallet-client = "0.3.0"
fedimint-mint-client = "0.3.0"
fedimint-ln-client = "0.3.0"
fedimint-rocksdb = "0.3.0"
url = "2.5.0"
lazy_static = "1.4.0"
async-utility = "0.2.0"
tower-http = { version = "0.5.2", features = ["cors", "auth", "trace"] }
bitcoin = "0.29.2"
itertools = "0.12.0"
lnurl-rs = { version = "0.5.0", features = ["async"], default-features = false }
reqwest = { version = "0.12.3", features = [
"json",
"rustls-tls",
], default-features = false }
lightning-invoice = { version = "0.26.0", features = ["serde"] }
bitcoin_hashes = "0.13.0"
time = { version = "0.3.25", features = ["formatting"] }
chrono = "0.4.31"
futures-util = "0.3.30"
clap = { version = "3", features = ["derive", "env"] }
multimint = { version = "0.3.2" }
# multimint = { path = "../multimint" }
axum-otel-metrics = "0.8.0"
base64 = "0.22.0"
hex = "0.4.3"
fedimint-tbs = "0.3.0"
1 change: 1 addition & 0 deletions clientd-stateless/examples/cashu_encoding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

49 changes: 49 additions & 0 deletions clientd-stateless/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::fmt;

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::json;

pub struct AppError {
pub error: anyhow::Error,
pub status: StatusCode,
}

impl AppError {
pub fn new(status: StatusCode, error: impl Into<anyhow::Error>) -> Self {
Self {
error: error.into(),
status,
}
}
}

// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(self.status, format!("Something went wrong: {}", self.error)).into_response()
}
}

impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let error_json = json!({
"error": self.error.to_string(),
"status": self.status.as_u16(),
});

write!(f, "{}", error_json)
}
}

impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self {
error: err.into(),
status: StatusCode::INTERNAL_SERVER_ERROR, // default status code
}
}
}
186 changes: 186 additions & 0 deletions clientd-stateless/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use std::path::PathBuf;
use std::str::FromStr;

use anyhow::Result;
use axum::http::Method;
use fedimint_core::api::InviteCode;
use router::handlers;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::info;

mod error;
mod router;
mod state;
mod utils;

use axum::routing::{get, post};
use axum::Router;
use axum_otel_metrics::HttpMetricsLayerBuilder;
use clap::{Parser, Subcommand};
use state::AppState;

#[derive(Subcommand)]
enum Commands {
Start,
Stop,
}

#[derive(Parser)]
#[clap(version = "1.0", author = "Kody Low")]
struct Cli {
/// Federation invite code
#[clap(long, env = "FEDIMINT_CLIENTD_INVITE_CODE", required = false)]
invite_code: String,

/// Path to FM database
#[clap(long, env = "FEDIMINT_CLIENTD_DB_PATH", required = true)]
db_path: PathBuf,

/// Password
#[clap(long, env = "FEDIMINT_CLIENTD_PASSWORD", required = true)]
password: String,

/// Addr
#[clap(long, env = "FEDIMINT_CLIENTD_ADDR", required = true)]
addr: String,

/// Manual secret
#[clap(long, env = "FEDIMINT_CLIENTD_MANUAL_SECRET", required = false)]
manual_secret: Option<String>,
}

// const PID_FILE: &str = "/tmp/fedimint_http.pid";

#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
dotenv::dotenv().ok();

let cli: Cli = Cli::parse();

let mut state = AppState::new(cli.db_path).await?;

let manual_secret = match cli.manual_secret {
Some(secret) => Some(secret),
None => match std::env::var("FEDIMINT_CLIENTD_MANUAL_SECRET") {
Ok(secret) => Some(secret),
Err(_) => None,
},
};

match InviteCode::from_str(&cli.invite_code) {
Ok(invite_code) => {
let federation_id = state
.multimint
.register_new(invite_code, manual_secret)
.await?;
info!("Created client for federation id: {:?}", federation_id);
}
Err(e) => {
info!(
"No federation invite code provided, skipping client creation: {}",
e
);
}
}

if state.multimint.all().await.is_empty() {
return Err(anyhow::anyhow!("No clients found, must have at least one client to start the server. Try providing a federation invite code with the `--invite-code` flag or setting the `FEDIMINT_CLIENTD_INVITE_CODE` environment variable."));
}

let app = Router::new().nest("/v1", cashu_v1_rest()).with_state(state);
info!("Starting stateless server");

let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
// allow auth header
.allow_headers(Any);

let metrics = HttpMetricsLayerBuilder::new()
.with_service_name("fedimint-clientd".to_string())
.build();

let app = app
.layer(cors)
.layer(TraceLayer::new_for_http()) // tracing requests
// no traces for routes bellow
.route("/health", get(|| async { "Server is up and running!" })) // for health check
// metrics for all routes above
.merge(metrics.routes())
.layer(metrics);

let listener = tokio::net::TcpListener::bind(format!("{}", &cli.addr))
.await
.map_err(|e| anyhow::anyhow!("Failed to bind to address, should be a valid address and port like 127.0.0.1:3333: {e}"))?;
info!("fedimint-clientd Listening on {}", &cli.addr);
axum::serve(listener, app)
.await
.map_err(|e| anyhow::anyhow!("Failed to start server: {e}"))?;

Ok(())
}

/// Implements Cashu V1 API Routes:
///
/// REQUIRED
/// NUT-01 Mint Public Key Exchange && NUT-02 Keysets and Keyset IDs
/// - `/cashu/v1/keys`
/// - `/cashu/v1/keys/{keyset_id}`
/// - `/cashu/v1/keysets`
/// NUT-03 Swap Tokens (Equivalent to `reissue` command)
/// - `/cashu/v1/swap`
/// NUT-04 Mint Tokens: supports `bolt11` and `onchain` methods
/// - `/cashu/v1/mint/quote/{method}`
/// - `/cashu/v1/mint/quote/{method}/{quote_id}`
/// - `/cashu/v1/mint/{method}`
/// NUT-05 Melting Tokens: supports `bolt11` and `onchain` methods
/// - `/cashu/v1/melt/quote/{method}`
/// - `/cashu/v1/melt/quote/{method}/{quote_id}`
/// - `/cashu/v1/melt/{method}`
/// NUT-06 Mint Information
/// - `/cashu/v1/info`
///
/// OPTIONAL
/// NUT-07 Token State Check
/// - `/cashu/v1/check`
/// NUT-08 Lightning Fee Return
/// - Modification of NUT-05 Melt
/// NUT-10 Spending Conditions
/// NUT-11 Pay to Public Key (P2PK)
/// - Fedimint already does this
/// NUT-12 Offline Ecash Signature Validation
/// - DLEQ in BlindedSignature for Mint to User
fn cashu_v1_rest() -> Router<AppState> {
Router::new()
.route("/keys", get(handlers::keys::handle_keys))
.route(
"/keys/:keyset_id",
get(handlers::keys::handle_keys_keyset_id),
)
.route("/keysets", get(handlers::keysets::handle_keysets))
.route("/swap", post(handlers::swap::handle_swap))
.route(
"/mint/quote/:method",
get(handlers::mint::quote::handle_method),
)
.route(
"/mint/quote/:method/:quote_id",
get(handlers::mint::quote::handle_method_quote_id),
)
.route("/mint/:method", post(handlers::mint::method::handle_method))
.route(
"/melt/quote/:method",
get(handlers::melt::quote::handle_method),
)
.route(
"/melt/quote/:method/:quote_id",
get(handlers::melt::quote::handle_method_quote_id),
)
.route("/melt/:method", post(handlers::melt::method::handle_method))
.route("/info", get(handlers::info::handle_info))
.route("/check", post(handlers::check::handle_check))
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::collections::BTreeMap;
use std::env;
use std::str::FromStr;

use axum::extract::State;
use axum::Json;
use fedimint_core::config::FederationId;
use serde::Serialize;

use crate::error::AppError;
Expand Down Expand Up @@ -42,7 +45,11 @@ pub struct CashuNUT06InfoResponse {
pub async fn handle_info(
State(state): State<AppState>,
) -> Result<Json<CashuNUT06InfoResponse>, AppError> {
let client = state.get_cashu_client().await?;
let federation_id = env::var("FEDIMINT_CLIENTD_ACTIVE_FEDERATION_ID")
.map_err(|e| anyhow::anyhow!("Failed to get active federation id: {}", e))?;
let client = state
.get_client(FederationId::from_str(&federation_id)?)
.await?;

let config = client.get_config();
let mut nuts = BTreeMap::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use tracing::{error, info};

use crate::error::AppError;
use crate::router::handlers::cashu::{Method, Unit};
use crate::router::handlers::{Method, Unit};
use crate::state::AppState;

#[derive(Debug, Deserialize)]
Expand Down
Loading

0 comments on commit 20615bd

Please sign in to comment.