diff --git a/.gitignore b/.gitignore index 0fcbff3..9c36fb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/target-nix .env fm_client_db .cargo diff --git a/JUSTFILE b/JUSTFILE index e345205..957d130 100644 --- a/JUSTFILE +++ b/JUSTFILE @@ -2,6 +2,9 @@ set dotenv-load := true dev: + mprocs -c mprocs.yaml + +mprocs: ./scripts/mprocs-nix.sh dev-fed mprocs-new.yaml alias b := build diff --git a/flake.lock b/flake.lock index 2454fbd..b61a2c5 100644 --- a/flake.lock +++ b/flake.lock @@ -530,15 +530,15 @@ }, "nixpkgs_5": { "locked": { - "lastModified": 1704420045, - "narHash": "sha256-C36QmoJd5tdQ5R9MC1jM7fBkZW9zBUqbUCsgwS6j4QU=", - "owner": "NixOS", + "lastModified": 1710695816, + "narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "c1be43e8e837b8dbee2b3665a007e761680f0c3d", + "rev": "614b4613980a522ba49f0d194531beddbb7220d3", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "nixos", "ref": "nixos-23.11", "repo": "nixpkgs", "type": "github" diff --git a/flake.nix b/flake.nix index b0420f4..365e42f 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,9 @@ description = "A fedimint client daemon for server side applications to hold, use, and manage Bitcoin"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; + nixpkgs = { + url = "github:nixos/nixpkgs/nixos-23.11"; + }; flakebox = { url = "github:rustshop/flakebox"; @@ -20,11 +22,13 @@ outputs = { self, nixpkgs, flakebox, flake-utils, fedimint }: flake-utils.lib.eachDefaultSystem (system: let + nixpkgs = fedimint.inputs.nixpkgs; pkgs = import nixpkgs { inherit system; overlays = fedimint.overlays.fedimint; }; lib = pkgs.lib; + fmLib = fedimint.lib.${system}; flakeboxLib = flakebox.lib.${system} { }; rustSrc = flakeboxLib.filterSubPaths { root = builtins.path { @@ -87,14 +91,15 @@ packages = { default = outputs.fedimint-clientd; }; - devShells = flakeboxLib.mkShells (commonArgs // { - toolchain = toolchainNative; - nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ - pkgs.mprocs - fedimint.packages.${system}.devimint - fedimint.packages.${system}.gateway-pkgs - fedimint.packages.${system}.fedimint-pkgs - ]; - }); + devShells = fmLib.devShells // { + default = fmLib.devShells.default.overrideAttrs (prev: { + nativeBuildInputs = [ + pkgs.mprocs + fedimint.packages.${system}.devimint + fedimint.packages.${system}.gateway-pkgs + fedimint.packages.${system}.fedimint-pkgs + ] ++ prev.nativeBuildInputs; + }); + }; }); } diff --git a/mprocs.yaml b/mprocs.yaml index 142b5c1..2377e42 100644 --- a/mprocs.yaml +++ b/mprocs.yaml @@ -2,7 +2,7 @@ procs: user: shell: zsh stop: SIGKILL - fedimint-http: + fedimint-clientd: shell: cargo run stop: SIGTERM ngrok: diff --git a/src/main.rs b/src/main.rs index cc34985..7a2b836 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,12 +24,11 @@ use state::AppState; // use tower_http::cors::{Any, CorsLayer}; use tower_http::validate_request::ValidateRequestHeaderLayer; -#[derive(Clone, Debug, ValueEnum)] +#[derive(Clone, Debug, ValueEnum, PartialEq)] enum Mode { - Fedimint, - Cashu, + Rest, Ws, - Default, + Cashu, } #[derive(Subcommand)] @@ -43,11 +42,11 @@ enum Commands { struct Cli { /// Federation invite code #[clap(long, env = "FEDIMINT_CLIENTD_INVITE_CODE", required = false)] - federation_invite_code: String, + invite_code: String, /// Path to FM database #[clap(long, env = "FEDIMINT_CLIENTD_DB_PATH", required = true)] - fm_db_path: PathBuf, + db_path: PathBuf, /// Password #[clap(long, env = "FEDIMINT_CLIENTD_PASSWORD", required = true)] @@ -57,8 +56,8 @@ struct Cli { #[clap(long, env = "FEDIMINT_CLIENTD_ADDR", required = true)] addr: String, - /// Mode of operation - #[clap(long, default_value = "default")] + /// Mode: ws, rest + #[clap(long, default_value = "rest")] mode: Mode, } @@ -70,11 +69,16 @@ async fn main() -> Result<()> { dotenv::dotenv().ok(); let cli: Cli = Cli::parse(); - let mut state = AppState::new(cli.fm_db_path).await?; - match InviteCode::from_str(&cli.federation_invite_code) { + + let mut state = AppState::new(cli.db_path).await?; + + match InviteCode::from_str(&cli.invite_code) { Ok(invite_code) => { let federation_id = state.multimint.register_new(invite_code, true).await?; info!("Created client for federation id: {:?}", federation_id); + if cli.mode == Mode::Cashu { + state.cashu_mint = Some(federation_id); + } } Err(e) => { info!( @@ -84,20 +88,23 @@ async fn main() -> Result<()> { } } + 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 = match cli.mode { - Mode::Fedimint => Router::new() - .nest("/fedimint/v2", fedimint_v2_rest()) + Mode::Rest => Router::new() + .nest("/v2", fedimint_v2_rest()) .with_state(state) .layer(ValidateRequestHeaderLayer::bearer(&cli.password)), - Mode::Cashu => Router::new() - .nest("/cashu/v1", cashu_v1_rest()) + Mode::Ws => Router::new() + .route("/ws", get(websocket_handler)) .with_state(state) .layer(ValidateRequestHeaderLayer::bearer(&cli.password)), - Mode::Ws => Router::new() - .route("/fedimint/v2/ws", get(websocket_handler)) + Mode::Cashu => Router::new() + .nest("/v1", cashu_v1_rest()) .with_state(state) .layer(ValidateRequestHeaderLayer::bearer(&cli.password)), - Mode::Default => create_default_router(state, &cli.password).await?, }; let cors = CorsLayer::new() @@ -132,23 +139,6 @@ async fn main() -> Result<()> { Ok(()) } -pub async fn create_default_router(state: AppState, password: &str) -> Result { - // TODO: Allow CORS? Probably not, since this should just interact with the - // local machine. let cors = CorsLayer::new() - // .allow_methods([Method::GET, Method::POST]) - // .allow_origin(Any); - - let app = Router::new() - .route("/fedimint/v2/ws", get(websocket_handler)) - .nest("/fedimint/v2", fedimint_v2_rest()) - .nest("/cashu/v1", cashu_v1_rest()) - .with_state(state) - // .layer(cors) - .layer(ValidateRequestHeaderLayer::bearer(password)); - - Ok(app) -} - /// Implements Fedimint V0.2 API Route matching against CLI commands: /// - `/fedimint/v2/admin/backup`: Upload the (encrypted) snapshot of mint notes /// to federation. diff --git a/src/router/handlers/cashu/info.rs b/src/router/handlers/cashu/info.rs index df1a793..18c2847 100644 --- a/src/router/handlers/cashu/info.rs +++ b/src/router/handlers/cashu/info.rs @@ -1,7 +1,6 @@ use std::collections::BTreeMap; use axum::extract::State; -use axum::http::StatusCode; use axum::Json; use serde::Serialize; @@ -43,15 +42,7 @@ pub struct CashuNUT06InfoResponse { pub async fn handle_info( State(state): State, ) -> Result, AppError> { - let client = match state.multimint.get_default().await { - Some(client) => client, - None => { - return Err(AppError::new( - StatusCode::INTERNAL_SERVER_ERROR, - anyhow::anyhow!("No default client"), - )) - } - }; + let client = state.get_cashu_client().await?; let config = client.get_config(); let mut nuts = BTreeMap::new(); diff --git a/src/router/handlers/cashu/melt/method.rs b/src/router/handlers/cashu/melt/method.rs index 476fe04..4ac5b00 100644 --- a/src/router/handlers/cashu/melt/method.rs +++ b/src/router/handlers/cashu/melt/method.rs @@ -23,7 +23,7 @@ pub struct PostMeltQuoteMethodRequest { pub request: String, pub amount: Amount, pub unit: Unit, - pub federation_id: Option, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] diff --git a/src/router/handlers/cashu/mint/method.rs b/src/router/handlers/cashu/mint/method.rs index c4d00c5..eec2baf 100644 --- a/src/router/handlers/cashu/mint/method.rs +++ b/src/router/handlers/cashu/mint/method.rs @@ -21,7 +21,7 @@ use crate::state::AppState; pub struct PostMintQuoteMethodRequest { pub amount: Amount, pub unit: Unit, - pub federation_id: Option, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] diff --git a/src/router/handlers/cashu/swap.rs b/src/router/handlers/cashu/swap.rs index 2128c38..3218626 100644 --- a/src/router/handlers/cashu/swap.rs +++ b/src/router/handlers/cashu/swap.rs @@ -15,7 +15,7 @@ use crate::state::AppState; #[derive(Debug, Deserialize)] pub struct SwapRequest { pub notes: OOBNotes, - pub federation_id: Option, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] diff --git a/src/router/handlers/fedimint/admin/backup.rs b/src/router/handlers/fedimint/admin/backup.rs index b1e6f11..97d3a10 100644 --- a/src/router/handlers/fedimint/admin/backup.rs +++ b/src/router/handlers/fedimint/admin/backup.rs @@ -17,7 +17,7 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct BackupRequest { pub metadata: BTreeMap, - pub federation_id: Option, + pub federation_id: FederationId, } async fn _backup(client: ClientArc, req: BackupRequest) -> Result<(), AppError> { diff --git a/src/router/handlers/fedimint/admin/list_operations.rs b/src/router/handlers/fedimint/admin/list_operations.rs index d33f15a..53d0183 100644 --- a/src/router/handlers/fedimint/admin/list_operations.rs +++ b/src/router/handlers/fedimint/admin/list_operations.rs @@ -18,7 +18,7 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct ListOperationsRequest { pub limit: usize, - pub federation_id: Option, + pub federation_id: FederationId, } #[derive(Serialize)] @@ -88,7 +88,7 @@ pub async fn handle_rest( State(state): State, Json(req): Json, ) -> Result, AppError> { - let client = state.get_client(None).await?; + let client = state.get_client(req.federation_id).await?; let operations = _list_operations(client, req).await?; Ok(Json(operations)) } diff --git a/src/router/handlers/fedimint/admin/module.rs b/src/router/handlers/fedimint/admin/module.rs index 6962eff..01e45fe 100644 --- a/src/router/handlers/fedimint/admin/module.rs +++ b/src/router/handlers/fedimint/admin/module.rs @@ -22,7 +22,7 @@ pub enum ModuleSelector { pub struct ModuleRequest { pub module: ModuleSelector, pub args: Vec, - pub federation_id: Option, + pub federation_id: FederationId, } async fn _module(_client: ClientArc, _req: ModuleRequest) -> Result<(), AppError> { diff --git a/src/router/handlers/fedimint/admin/restore.rs b/src/router/handlers/fedimint/admin/restore.rs index e348904..4878a86 100644 --- a/src/router/handlers/fedimint/admin/restore.rs +++ b/src/router/handlers/fedimint/admin/restore.rs @@ -16,18 +16,18 @@ async fn _restore(_client: ClientArc, _v: Value) -> Result<(), AppError> { )) } -pub async fn handle_ws(state: AppState, v: Value) -> Result { - let client = state.get_client(None).await?; - _restore(client, v).await?; - Ok(json!(())) +pub async fn handle_ws(_state: AppState, _v: Value) -> Result { + // let client = state.get_client(v).await?; + // _restore(client, v).await?; + Ok(json!(null)) } #[axum_macros::debug_handler] pub async fn handle_rest( - State(state): State, - Json(req): Json, + State(_state): State, + Json(_req): Json, ) -> Result, AppError> { - let client = state.get_client(None).await?; - _restore(client, req).await?; + // let client = state.get_client(None).await?; + // _restore(client, req).await?; Ok(Json(())) } diff --git a/src/router/handlers/fedimint/ln/await_invoice.rs b/src/router/handlers/fedimint/ln/await_invoice.rs index 45f5ec5..cfaf0d0 100644 --- a/src/router/handlers/fedimint/ln/await_invoice.rs +++ b/src/router/handlers/fedimint/ln/await_invoice.rs @@ -20,7 +20,7 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct AwaitInvoiceRequest { pub operation_id: OperationId, - pub federation_id: Option, + pub federation_id: FederationId, } async fn _await_invoice( diff --git a/src/router/handlers/fedimint/ln/await_pay.rs b/src/router/handlers/fedimint/ln/await_pay.rs index 1f5c9fa..a4ee3ec 100644 --- a/src/router/handlers/fedimint/ln/await_pay.rs +++ b/src/router/handlers/fedimint/ln/await_pay.rs @@ -18,7 +18,7 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct AwaitLnPayRequest { pub operation_id: OperationId, - pub federation_id: Option, + pub federation_id: FederationId, } async fn _await_pay(client: ClientArc, req: AwaitLnPayRequest) -> Result { diff --git a/src/router/handlers/fedimint/ln/invoice.rs b/src/router/handlers/fedimint/ln/invoice.rs index 8120094..9e46357 100644 --- a/src/router/handlers/fedimint/ln/invoice.rs +++ b/src/router/handlers/fedimint/ln/invoice.rs @@ -19,7 +19,7 @@ pub struct LnInvoiceRequest { pub amount_msat: Amount, pub description: String, pub expiry_time: Option, - pub federation_id: Option, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] diff --git a/src/router/handlers/fedimint/ln/list_gateways.rs b/src/router/handlers/fedimint/ln/list_gateways.rs index 44748be..28ce7d1 100644 --- a/src/router/handlers/fedimint/ln/list_gateways.rs +++ b/src/router/handlers/fedimint/ln/list_gateways.rs @@ -14,7 +14,7 @@ use crate::state::AppState; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ListGatewaysRequest { - pub federation_id: Option, + pub federation_id: FederationId, } async fn _list_gateways(client: ClientArc) -> Result { diff --git a/src/router/handlers/fedimint/ln/pay.rs b/src/router/handlers/fedimint/ln/pay.rs index 33c5c6e..556e30f 100644 --- a/src/router/handlers/fedimint/ln/pay.rs +++ b/src/router/handlers/fedimint/ln/pay.rs @@ -22,7 +22,7 @@ pub struct LnPayRequest { pub amount_msat: Option, pub finish_in_background: bool, pub lnurl_comment: Option, - pub federeation_id: Option, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] @@ -68,7 +68,7 @@ async fn _pay(client: ClientArc, req: LnPayRequest) -> Result Result { let v = serde_json::from_value::(v) .map_err(|e| AppError::new(StatusCode::BAD_REQUEST, anyhow!("Invalid request: {}", e)))?; - let client = state.get_client(v.federeation_id).await?; + let client = state.get_client(v.federation_id).await?; let pay = _pay(client, v).await?; let pay_json = json!(pay); Ok(pay_json) @@ -79,7 +79,7 @@ pub async fn handle_rest( State(state): State, Json(req): Json, ) -> Result, AppError> { - let client = state.get_client(req.federeation_id).await?; + let client = state.get_client(req.federation_id).await?; let pay = _pay(client, req).await?; Ok(Json(pay)) } diff --git a/src/router/handlers/fedimint/ln/switch_gateway.rs b/src/router/handlers/fedimint/ln/switch_gateway.rs index f05bf2d..2509f09 100644 --- a/src/router/handlers/fedimint/ln/switch_gateway.rs +++ b/src/router/handlers/fedimint/ln/switch_gateway.rs @@ -18,7 +18,7 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct SwitchGatewayRequest { pub gateway_id: String, - pub federation_id: Option, + pub federation_id: FederationId, } async fn _switch_gateway(client: ClientArc, req: SwitchGatewayRequest) -> Result { @@ -34,7 +34,7 @@ async fn _switch_gateway(client: ClientArc, req: SwitchGatewayRequest) -> Result pub async fn handle_ws(state: AppState, v: Value) -> Result { let v = serde_json::from_value::(v) .map_err(|e| AppError::new(StatusCode::BAD_REQUEST, anyhow!("Invalid request: {}", e)))?; - let client = state.get_client(None).await?; + let client = state.get_client(v.federation_id).await?; let gateway = _switch_gateway(client, v).await?; let gateway_json = json!(gateway); Ok(gateway_json) diff --git a/src/router/handlers/fedimint/mint/spend.rs b/src/router/handlers/fedimint/mint/spend.rs index c94423c..6219f6d 100644 --- a/src/router/handlers/fedimint/mint/spend.rs +++ b/src/router/handlers/fedimint/mint/spend.rs @@ -22,7 +22,7 @@ pub struct SpendRequest { pub amount_msat: Amount, pub allow_overpay: bool, pub timeout: u64, - pub federation_id: Option, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] diff --git a/src/router/handlers/fedimint/wallet/await_deposit.rs b/src/router/handlers/fedimint/wallet/await_deposit.rs index 964dae8..b14719a 100644 --- a/src/router/handlers/fedimint/wallet/await_deposit.rs +++ b/src/router/handlers/fedimint/wallet/await_deposit.rs @@ -17,7 +17,7 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct AwaitDepositRequest { pub operation_id: OperationId, - pub federation_id: Option, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] diff --git a/src/router/handlers/fedimint/wallet/deposit_address.rs b/src/router/handlers/fedimint/wallet/deposit_address.rs index f37a74e..4967a44 100644 --- a/src/router/handlers/fedimint/wallet/deposit_address.rs +++ b/src/router/handlers/fedimint/wallet/deposit_address.rs @@ -20,7 +20,7 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct DepositAddressRequest { pub timeout: u64, - pub federation_id: Option, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] diff --git a/src/router/handlers/fedimint/wallet/withdraw.rs b/src/router/handlers/fedimint/wallet/withdraw.rs index ea40da8..3700aea 100644 --- a/src/router/handlers/fedimint/wallet/withdraw.rs +++ b/src/router/handlers/fedimint/wallet/withdraw.rs @@ -20,8 +20,8 @@ use crate::state::AppState; #[serde(rename_all = "camelCase")] pub struct WithdrawRequest { pub address: Address, - pub amount_msat: BitcoinAmountOrAll, - pub federation_id: Option, + pub amount_sat: BitcoinAmountOrAll, + pub federation_id: FederationId, } #[derive(Debug, Serialize)] @@ -33,7 +33,7 @@ pub struct WithdrawResponse { async fn _withdraw(client: ClientArc, req: WithdrawRequest) -> Result { let wallet_module = client.get_first_module::(); - let (amount, fees) = match req.amount_msat { + let (amount, fees) = match req.amount_sat { // If the amount is "all", then we need to subtract the fees from // the amount we are withdrawing BitcoinAmountOrAll::All => { diff --git a/src/state.rs b/src/state.rs index 14b648a..b9bed93 100644 --- a/src/state.rs +++ b/src/state.rs @@ -10,25 +10,21 @@ use crate::error::AppError; #[derive(Debug, Clone)] pub struct AppState { pub multimint: MultiMint, + pub cashu_mint: Option, } impl AppState { pub async fn new(fm_db_path: PathBuf) -> Result { let clients = MultiMint::new(fm_db_path).await?; - Ok(Self { multimint: clients }) + Ok(Self { + multimint: clients, + cashu_mint: None, + }) } // Helper function to get a specific client from the state or default - pub async fn get_client( - &self, - federation_id: Option, - ) -> Result { - let client = match federation_id { - Some(federation_id) => self.multimint.get(&federation_id).await, - None => self.multimint.get_default().await, - }; - - match client { + pub async fn get_client(&self, federation_id: FederationId) -> Result { + match self.multimint.get(&federation_id).await { Some(client) => Ok(client), None => Err(AppError::new( StatusCode::BAD_REQUEST, @@ -37,6 +33,22 @@ impl AppState { } } + pub async fn get_cashu_client(&self) -> Result { + match self.cashu_mint { + Some(client) => match self.multimint.get(&client).await { + Some(client) => Ok(client), + None => Err(AppError::new( + StatusCode::BAD_REQUEST, + anyhow!("No cashu client found for federation id"), + )), + }, + None => Err(AppError::new( + StatusCode::BAD_REQUEST, + anyhow!("No cashu client set"), + )), + } + } + pub async fn get_client_by_prefix( &self, federation_id_prefix: &FederationIdPrefix,