-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #34 from fedimint/stateless-clientd
- Loading branch information
Showing
58 changed files
with
638 additions
and
213 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
Oops, something went wrong.