diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ae7e9401..fce36c60f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,8 +123,6 @@ jobs: uses: DeterminateSystems/magic-nix-cache-action@v6 - name: Rust Cache uses: Swatinem/rust-cache@v2 - - name: Clippy - run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings - name: Test run: nix develop -i -L .#stable --command just itest ${{ matrix.database }} @@ -153,9 +151,11 @@ jobs: - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Clippy - run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings + run: nix develop -i -L .#stable --command cargo clippy -- -D warnings - name: Test fake mint run: nix develop -i -L .#stable --command just fake-mint-itest ${{ matrix.database }} + - name: Test Mint tests + run: nix develop -i -L .#stable --command cargo test -p cdk-integration-tests --test mint msrv-build: name: "MSRV build" diff --git a/.helix/languages.toml b/.helix/languages.toml index c2a98dd65..85046ad7b 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,2 +1,8 @@ [language-server.rust-analyzer.config] cargo = { features = ["wallet", "mint", "swagger", "redis"] } +inlayHints.bindingModeHints.enable = true +inlayHints.closingBraceHints.enable = true +inlayHints.chainingHints.enable = true +inlayHints.parameterHints.enable = true +inlayHints.typeHints.enable = true +command = "rust-analyzer" diff --git a/Cargo.lock b/Cargo.lock index 81140bfb9..366bfb782 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -740,9 +740,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.8" +version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" +checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" dependencies = [ "jobserver", "libc", @@ -765,6 +765,7 @@ dependencies = [ "criterion", "futures", "getrandom", + "jsonwebtoken", "lightning-invoice", "rand", "regex", @@ -1005,6 +1006,7 @@ name = "cdk-redb" version = "0.6.0" dependencies = [ "async-trait", + "cashu", "cdk-common", "lightning-invoice", "redb", @@ -1036,6 +1038,7 @@ version = "0.6.0" dependencies = [ "async-trait", "bitcoin 0.32.5", + "cashu", "cdk-common", "lightning-invoice", "serde_json", @@ -1548,16 +1551,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "erased-serde" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" -dependencies = [ - "serde", - "typeid", -] - [[package]] name = "errno" version = "0.3.10" @@ -2476,6 +2469,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring 0.17.8", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2603,12 +2611,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.24" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d" -dependencies = [ - "value-bag", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" @@ -3040,6 +3045,16 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3228,9 +3243,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" +checksum = "924b9a625d6df5b74b0b3cfbb5669b3f62ddf3d46a677ce12b1945471b4ae5c3" dependencies = [ "proc-macro2", "syn 2.0.96", @@ -4076,15 +4091,6 @@ dependencies = [ "syn 2.0.96", ] -[[package]] -name = "serde_fmt" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" -dependencies = [ - "serde", -] - [[package]] name = "serde_json" version = "1.0.135" @@ -4211,6 +4217,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 1.0.69", + "time", +] + [[package]] name = "skeptic" version = "0.13.7" @@ -4425,84 +4443,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "sval" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8" - -[[package]] -name = "sval_buffer" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f" -dependencies = [ - "sval", - "sval_ref", -] - -[[package]] -name = "sval_dynamic" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_fmt" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_json" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155" -dependencies = [ - "itoa", - "ryu", - "sval", -] - -[[package]] -name = "sval_nested" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f" -dependencies = [ - "sval", - "sval_buffer", - "sval_ref", -] - -[[package]] -name = "sval_ref" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa" -dependencies = [ - "sval", -] - -[[package]] -name = "sval_serde" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a" -dependencies = [ - "serde", - "sval", - "sval_nested", -] - [[package]] name = "syn" version = "1.0.109" @@ -5091,12 +5031,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "typeid" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" - [[package]] name = "typenum" version = "1.17.0" @@ -5268,42 +5202,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "value-bag" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" -dependencies = [ - "value-bag-serde1", - "value-bag-sval2", -] - -[[package]] -name = "value-bag-serde1" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b" -dependencies = [ - "erased-serde", - "serde", - "serde_fmt", -] - -[[package]] -name = "value-bag-sval2" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a" -dependencies = [ - "sval", - "sval_buffer", - "sval_dynamic", - "sval_fmt", - "sval_json", - "sval_ref", - "sval_serde", -] - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/crates/cashu/src/nuts/mod.rs b/crates/cashu/src/nuts/mod.rs index 0e09da994..d53b8b473 100644 --- a/crates/cashu/src/nuts/mod.rs +++ b/crates/cashu/src/nuts/mod.rs @@ -23,6 +23,8 @@ pub mod nut17; pub mod nut18; pub mod nut19; pub mod nut20; +pub mod nutxx; +pub mod nutxx1; pub use nut00::{ BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proof, Proofs, ProofsMethods, @@ -55,3 +57,5 @@ pub use nut14::HTLCWitness; pub use nut15::{Mpp, MppMethodSettings, Settings as NUT15Settings}; pub use nut17::NotificationPayload; pub use nut18::{PaymentRequest, PaymentRequestPayload, Transport}; +pub use nutxx::{Method, ProtectedEndpoint, RoutePath}; +pub use nutxx1::{AuthProof, AuthRequired, AuthToken, BlindAuthToken}; diff --git a/crates/cashu/src/nuts/nut00/mod.rs b/crates/cashu/src/nuts/nut00/mod.rs index 4d8bbb81b..d88e2e33e 100644 --- a/crates/cashu/src/nuts/nut00/mod.rs +++ b/crates/cashu/src/nuts/nut00/mod.rs @@ -382,6 +382,8 @@ pub enum CurrencyUnit { Usd, /// Euro Eur, + /// Auth + Auth, /// Custom currency unit Custom(String), } @@ -395,6 +397,7 @@ impl CurrencyUnit { Self::Msat => Some(1), Self::Usd => Some(2), Self::Eur => Some(3), + Self::Auth => Some(4), _ => None, } } @@ -409,6 +412,7 @@ impl FromStr for CurrencyUnit { "MSAT" => Ok(Self::Msat), "USD" => Ok(Self::Usd), "EUR" => Ok(Self::Eur), + "AUTH" => Ok(Self::Auth), c => Ok(Self::Custom(c.to_string())), } } @@ -421,6 +425,7 @@ impl fmt::Display for CurrencyUnit { CurrencyUnit::Msat => "MSAT", CurrencyUnit::Usd => "USD", CurrencyUnit::Eur => "EUR", + CurrencyUnit::Auth => "AUTH", CurrencyUnit::Custom(unit) => unit, }; if let Some(width) = f.width() { diff --git a/crates/cashu/src/nuts/nut06.rs b/crates/cashu/src/nuts/nut06.rs index d8cce86b3..e6041a90b 100644 --- a/crates/cashu/src/nuts/nut06.rs +++ b/crates/cashu/src/nuts/nut06.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use super::nut01::PublicKey; use super::nut17::SupportedMethods; use super::nut19::CachedEndpoint; -use super::{nut04, nut05, nut15, nut19, MppMethodSettings}; +use super::{nut04, nut05, nut15, nut19, nutxx, nutxx1, MppMethodSettings}; /// Mint Version #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -249,6 +249,16 @@ pub struct Nuts { #[serde(default)] #[serde(rename = "20")] pub nut20: SupportedSettings, + /// NUTXX Settings + #[serde(default)] + #[serde(rename = "XX")] + #[serde(skip_serializing_if = "Option::is_none")] + pub nutxx: Option, + /// NUTXX1 Settings + #[serde(default)] + #[serde(rename = "XX+1")] + #[serde(skip_serializing_if = "Option::is_none")] + pub nutxx1: Option, } impl Nuts { diff --git a/crates/cashu/src/nuts/nutxx/mod.rs b/crates/cashu/src/nuts/nutxx/mod.rs new file mode 100644 index 000000000..9b3a8aa70 --- /dev/null +++ b/crates/cashu/src/nuts/nutxx/mod.rs @@ -0,0 +1,90 @@ +//! XX Clear Auth + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// NUTXX Error +#[derive(Debug, Error)] +pub enum Error {} + +/// Clear Auth Settings +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] +pub struct Settings { + /// Openid discovery + pub openid_discovery: String, + /// Client ID + pub client_id: String, + /// Protected endpioints + pub protected_endpoints: Vec, +} + +impl Settings { + /// Create new [`Settings`] + pub fn new( + openid_discovery: String, + client_id: String, + protected_endpoints: Vec, + ) -> Self { + Self { + openid_discovery, + client_id, + protected_endpoints, + } + } +} + +/// List of the methods and paths that are protected +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ProtectedEndpoint { + /// HTTP Method + pub method: Method, + /// Route path + pub path: RoutePath, +} + +impl ProtectedEndpoint { + /// Create [`CachedEndpoint`] + pub fn new(method: Method, path: RoutePath) -> Self { + Self { method, path } + } +} + +/// HTTP method +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum Method { + /// Get + Get, + /// POST + Post, +} + +/// Route path +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RoutePath { + /// Bolt11 Mint Quote + #[serde(rename = "/v1/mint/quote/bolt11")] + MintQuoteBolt11, + /// Bolt11 Mint + #[serde(rename = "/v1/mint/bolt11")] + MintBolt11, + /// Bolt11 Melt Quote + #[serde(rename = "/v1/melt/quote/bolt11")] + MeltQuoteBolt11, + /// Bolt11 Melt + #[serde(rename = "/v1/melt/bolt11")] + MeltBolt11, + /// Swap + #[serde(rename = "/v1/swap")] + Swap, + /// Checkstate + #[serde(rename = "/v1/checkstate")] + Checkstate, + /// Restore + #[serde(rename = "/v1/restore")] + Restore, + /// Mint Blind Auth + #[serde(rename = "/v1/auth/blind/mint/")] + MintBlindAuth, +} diff --git a/crates/cashu/src/nuts/nutxx1.rs b/crates/cashu/src/nuts/nutxx1.rs new file mode 100644 index 000000000..11359bb7f --- /dev/null +++ b/crates/cashu/src/nuts/nutxx1.rs @@ -0,0 +1,200 @@ +//! XX+1 Blind Auth + +use std::fmt; + +use bitcoin::base64::engine::general_purpose; +use bitcoin::base64::Engine; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use super::nutxx::ProtectedEndpoint; +use super::{BlindedMessage, Id, Proof, PublicKey}; +use crate::dhke::hash_to_curve; +use crate::secret::Secret; +use crate::util::hex; + +/// NUTxx1 Error +#[derive(Debug, Error)] +pub enum Error { + /// Invalid Prefix + #[error("Invalid prefix")] + InvalidPrefix, + /// Hex Error + #[error(transparent)] + HexError(#[from] hex::Error), + /// Base64 error + #[error(transparent)] + Base64Error(#[from] bitcoin::base64::DecodeError), + /// Serde Json error + #[error(transparent)] + SerdeJsonError(#[from] serde_json::Error), + /// Utf8 parse error + #[error(transparent)] + Utf8ParseError(#[from] std::string::FromUtf8Error), + /// DHKE error + #[error(transparent)] + DHKE(#[from] crate::dhke::Error), +} + +/// Blind auth settings +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] +pub struct Settings { + /// Max number of blind auth toeksn that can be minted per request + pub bat_max_mint: u64, + /// Protected endpoints + pub protected_endpoints: Vec, +} + +impl Settings { + /// Create new [`Settings`] + pub fn new(bat_max_mint: u64, protected_endpoints: Vec) -> Self { + Self { + bat_max_mint, + protected_endpoints, + } + } +} + +/// Auth Token +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AuthToken { + /// Clear Auth token + ClearAuth(String), + /// Blind Auth token + BlindAuth(BlindAuthToken), +} + +impl fmt::Display for AuthToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ClearAuth(cat) => { + write!(f, "{}", cat) + } + Self::BlindAuth(bat) => { + write!(f, "{}", bat) + } + } + } +} + +impl AuthToken { + /// Header key for auth token type + pub fn header_key(&self) -> String { + match self { + Self::ClearAuth(_) => "Clear-auth".to_string(), + Self::BlindAuth(_) => "Blind-auth".to_string(), + } + } +} + +/// Required Auth +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AuthRequired { + /// Clear Auth token + Clear, + /// Blind Auth token + Blind, +} + +/// Auth Proofs +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] +pub struct AuthProof { + /// `Keyset id` + #[serde(rename = "id")] + pub keyset_id: Id, + /// Secret message + #[cfg_attr(feature = "swagger", schema(value_type = String))] + pub secret: Secret, + /// Unblinded signature + #[serde(rename = "C")] + #[cfg_attr(feature = "swagger", schema(value_type = String))] + pub c: PublicKey, +} + +impl AuthProof { + /// Y of AuthProof + pub fn y(&self) -> Result { + Ok(hash_to_curve(self.secret.as_bytes())?) + } +} + +impl From for Proof { + fn from(value: AuthProof) -> Self { + Self { + amount: 1.into(), + keyset_id: value.keyset_id, + secret: value.secret, + c: value.c, + witness: None, + dleq: None, + } + } +} + +impl From for AuthProof { + fn from(value: Proof) -> Self { + Self { + keyset_id: value.keyset_id, + secret: value.secret, + c: value.c, + } + } +} + +/// Blind Auth Token +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct BlindAuthToken { + /// [AuthProof] + pub auth_proof: AuthProof, +} + +impl fmt::Display for BlindAuthToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let json_string = serde_json::to_string(&self.auth_proof).map_err(|_| fmt::Error)?; + let encoded = general_purpose::URL_SAFE.encode(json_string); + write!(f, "authA{}", encoded) + } +} + +impl std::str::FromStr for BlindAuthToken { + type Err = Error; + + fn from_str(s: &str) -> Result { + // Check if string starts with the expected prefix + if !s.starts_with("authA") { + return Err(Error::InvalidPrefix); + } + + // Remove the prefix to get the base64 encoded part + let encoded = &s["authA".len()..]; + + // Decode the base64 URL-safe string + let json_string = general_purpose::URL_SAFE.decode(encoded)?; + + // Convert bytes to UTF-8 string + let json_str = String::from_utf8(json_string)?; + + // Deserialize the JSON string into AuthProof + let auth_proof: AuthProof = serde_json::from_str(&json_str)?; + + Ok(BlindAuthToken { auth_proof }) + } +} + +/// Mint auth request [NUT-XX] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] +pub struct MintAuthRequest { + /// Outputs + #[cfg_attr(feature = "swagger", schema(max_items = 1_000))] + pub outputs: Vec, +} + +impl MintAuthRequest { + /// Count of tokens + pub fn amount(&self) -> u64 { + self.outputs.len() as u64 + } +} diff --git a/crates/cdk-axum/src/auth.rs b/crates/cdk-axum/src/auth.rs new file mode 100644 index 000000000..3e29cb838 --- /dev/null +++ b/crates/cdk-axum/src/auth.rs @@ -0,0 +1,188 @@ +use std::str::FromStr; + +use axum::extract::{FromRequestParts, State}; +use axum::http::request::Parts; +use axum::http::StatusCode; +use axum::response::Response; +use axum::routing::{get, post}; +use axum::{async_trait, Json, Router}; +use cdk::error::{ErrorCode, ErrorResponse}; +use cdk::nuts::nutxx1::MintAuthRequest; +use cdk::nuts::{AuthToken, BlindAuthToken, KeysResponse, KeysetResponse, MintBolt11Response}; +use serde::{Deserialize, Serialize}; + +use crate::{get_keyset_pubkeys, into_response, MintState}; + +const CLEAR_AUTH_KEY: &str = "Clear-auth"; +const BLIND_AUTH_KEY: &str = "Blind-auth"; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum AuthHeader { + /// Clear Auth token + Clear(String), + /// Blind Auth token + Blind(BlindAuthToken), + /// No auth + None, +} + +impl From for Option { + fn from(value: AuthHeader) -> Option { + match value { + AuthHeader::Clear(token) => Some(AuthToken::ClearAuth(token)), + AuthHeader::Blind(token) => Some(AuthToken::BlindAuth(token)), + AuthHeader::None => None, + } + } +} + +#[async_trait] +impl FromRequestParts for AuthHeader +where + S: Send + Sync, +{ + type Rejection = (StatusCode, String); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + // Check for Blind-auth header + if let Some(bat) = parts.headers.get(BLIND_AUTH_KEY) { + let token = bat + .to_str() + .map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "Invalid Blind-auth header value".to_string(), + ) + })? + .to_string(); + + let token = BlindAuthToken::from_str(&token).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "Invalid Blind-auth header value".to_string(), + ) + })?; + + return Ok(AuthHeader::Blind(token)); + } + + // Check for Clear-auth header + if let Some(cat) = parts.headers.get(CLEAR_AUTH_KEY) { + let token = cat + .to_str() + .map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "Invalid Clear-auth header value".to_string(), + ) + })? + .to_string(); + return Ok(AuthHeader::Clear(token)); + } + + // No authentication headers found - this is now valid + Ok(AuthHeader::None) + } +} + +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1/auth/blind", + path = "/keysets", + responses( + (status = 200, description = "Successful response", body = KeysetResponse, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +/// Get all active keyset IDs of the mint +/// +/// This endpoint returns a list of keysets that the mint currently supports and will accept tokens from. +pub async fn get_auth_keysets( + State(state): State, +) -> Result, Response> { + let keysets = state.mint.auth_keysets().await.map_err(|err| { + tracing::error!("Could not get keysets: {}", err); + into_response(err) + })?; + + Ok(Json(keysets)) +} + +#[cfg_attr(feature = "swagger", utoipa::path( + get, + context_path = "/v1/auth/blind", + path = "/keys", + responses( + (status = 200, description = "Successful response", body = KeysResponse, content_type = "application/json") + ) +))] +/// Get the public keys of the newest blind auth mint keyset +/// +/// This endpoint returns a dictionary of all supported token values of the mint and their associated public key. +pub async fn get_blind_auth_keys( + State(state): State, +) -> Result, Response> { + let pubkeys = state.mint.auth_pubkeys().await.map_err(|err| { + tracing::error!("Could not get keys: {}", err); + into_response(err) + })?; + + Ok(Json(pubkeys)) +} + +/// Mint tokens by paying a BOLT11 Lightning invoice. +/// +/// Requests the minting of tokens belonging to a paid payment request. +/// +/// Call this endpoint after `POST /v1/mint/quote`. +#[cfg_attr(feature = "swagger", utoipa::path( + post, + context_path = "/v1/auth", + path = "/blind/mint", + request_body(content = MintAuthRequest, description = "Request params", content_type = "application/json"), + responses( + (status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"), + (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") + ) +))] +pub async fn post_mint_auth( + auth: AuthHeader, + State(state): State, + Json(payload): Json, +) -> Result, Response> { + let auth_token = match auth { + AuthHeader::Clear(cat) => AuthToken::ClearAuth(cat), + _ => { + tracing::debug!("Received blind auth mint request without cat"); + return Err(into_response(ErrorResponse::new( + ErrorCode::from_code(0), + None, + None, + ))); + } + }; + + let res = state + .mint + .mint_blind_auth(auth_token, payload) + .await + .map_err(|err| { + tracing::error!("Could not process blind auth mint: {}", err); + into_response(err) + })?; + + Ok(Json(res)) +} + +pub fn create_auth_router(state: MintState) -> Router { + Router::new() + .nest( + "/auth/blind", + Router::new() + .route("/keys", get(get_blind_auth_keys)) + .route("/keysets", get(get_auth_keysets)) + .route("/keys/:keyset_id", get(get_keyset_pubkeys)) + .route("/mint", post(post_mint_auth)), + ) + .with_state(state) +} diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index b5b2ed24a..2de124600 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -6,12 +6,14 @@ use std::sync::Arc; use anyhow::Result; +use auth::create_auth_router; use axum::routing::{get, post}; use axum::Router; use cache::HttpCache; use cdk::mint::Mint; use router_handlers::*; +mod auth; pub mod cache; mod router_handlers; mod ws; @@ -169,7 +171,12 @@ pub async fn create_mint_router_with_custom_cache( .route("/info", get(get_mint_info)) .route("/restore", post(post_restore)); - let mint_router = Router::new().nest("/v1", v1_router).with_state(state); + let auth_router = create_auth_router(state.clone()); + + let mint_router = Router::new() + .nest("/v1", v1_router) + .nest("/v1", auth_router) + .with_state(state); Ok(mint_router) } diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 5c5710598..8e35f8503 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -14,6 +14,7 @@ use cdk::util::unix_time; use paste::paste; use uuid::Uuid; +use crate::auth::AuthHeader; use crate::ws::main_websocket; use crate::MintState; @@ -23,26 +24,24 @@ macro_rules! post_cache_wrapper { /// Cache wrapper function for $handler: /// Wrap $handler into a function that caches responses using the request as key pub async fn []( + auth: AuthHeader, state: State, payload: Json<$request_type> ) -> Result, Response> { use std::ops::Deref; - let json_extracted_payload = payload.deref(); let State(mint_state) = state.clone(); let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) { Some(key) => key, None => { // Could not calculate key, just return the handler result - return $handler(state, payload).await; + return $handler(auth, state, payload).await; } }; - if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await { return Ok(Json(cached_response)); } - - let response = $handler(state, payload).await?; + let response = $handler(auth, state, payload).await?; mint_state.cache.set(cache_key, &response.deref()).await; Ok(response) } @@ -144,12 +143,13 @@ pub async fn get_keysets(State(state): State) -> Result, Json(payload): Json, ) -> Result>, Response> { let quote = state .mint - .get_mint_bolt11_quote(payload) + .get_mint_bolt11_quote(auth.into(), payload) .await .map_err(into_response)?; @@ -172,12 +172,13 @@ pub async fn post_mint_bolt11_quote( /// /// Get mint quote state. pub async fn get_check_mint_bolt11_quote( + auth: AuthHeader, State(state): State, Path(quote_id): Path, ) -> Result>, Response> { let quote = state .mint - .check_mint_quote("e_id) + .check_mint_quote(auth.into(), "e_id) .await .map_err(|err| { tracing::error!("Could not check mint quote {}: {}", quote_id, err); @@ -207,12 +208,13 @@ pub async fn ws_handler(State(state): State, ws: WebSocketUpgrade) -> ) ))] pub async fn post_mint_bolt11( + auth: AuthHeader, State(state): State, Json(payload): Json>, ) -> Result, Response> { let res = state .mint - .process_mint_request(payload) + .process_mint_request(auth.into(), payload) .await .map_err(|err| { tracing::error!("Could not process mint: {}", err); @@ -234,12 +236,13 @@ pub async fn post_mint_bolt11( ))] /// Request a quote for melting tokens pub async fn post_melt_bolt11_quote( + auth: AuthHeader, State(state): State, Json(payload): Json, ) -> Result>, Response> { let quote = state .mint - .get_melt_bolt11_quote(&payload) + .get_melt_bolt11_quote(auth.into(), &payload) .await .map_err(into_response)?; @@ -262,12 +265,13 @@ pub async fn post_melt_bolt11_quote( /// /// Get melt quote state. pub async fn get_check_melt_bolt11_quote( + auth: AuthHeader, State(state): State, Path(quote_id): Path, ) -> Result>, Response> { let quote = state .mint - .check_melt_quote("e_id) + .check_melt_quote(auth.into(), "e_id) .await .map_err(|err| { tracing::error!("Could not check melt quote: {}", err); @@ -291,12 +295,13 @@ pub async fn get_check_melt_bolt11_quote( /// /// Requests tokens to be destroyed and sent out via Lightning. pub async fn post_melt_bolt11( + auth: AuthHeader, State(state): State, Json(payload): Json>, ) -> Result>, Response> { let res = state .mint - .melt_bolt11(&payload) + .melt_bolt11(auth.into(), &payload) .await .map_err(into_response)?; @@ -317,13 +322,18 @@ pub async fn post_melt_bolt11( /// /// Check whether a secret has been spent already or not. pub async fn post_check( + auth: AuthHeader, State(state): State, Json(payload): Json, ) -> Result, Response> { - let state = state.mint.check_state(&payload).await.map_err(|err| { - tracing::error!("Could not check state of proofs"); - into_response(err) - })?; + let state = state + .mint + .check_state(auth.into(), &payload) + .await + .map_err(|err| { + tracing::error!("Could not check state of proofs"); + into_response(err) + })?; Ok(Json(state)) } @@ -357,17 +367,19 @@ pub async fn get_mint_info(State(state): State) -> Result, Json(payload): Json, ) -> Result, Response> { let swap_response = state .mint - .process_swap_request(payload) + .process_swap_request(auth.into(), payload) .await .map_err(|err| { tracing::error!("Could not process swap request: {}", err); into_response(err) })?; + Ok(Json(swap_response)) } @@ -383,13 +395,18 @@ pub async fn post_swap( ))] /// Restores blind signature for a set of outputs. pub async fn post_restore( + auth: AuthHeader, State(state): State, Json(payload): Json, ) -> Result, Response> { - let restore_response = state.mint.restore(payload).await.map_err(|err| { - tracing::error!("Could not process restore: {}", err); - into_response(err) - })?; + let restore_response = state + .mint + .restore(auth.into(), payload) + .await + .map_err(|err| { + tracing::error!("Could not process restore: {}", err); + into_response(err) + })?; Ok(Json(restore_response)) } diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 5352efd56..fa6e9cb8c 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -7,8 +7,7 @@ use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::cdk_database; use cdk::cdk_database::WalletDatabase; -use cdk::wallet::client::HttpClient; -use cdk::wallet::{MultiMintWallet, Wallet}; +use cdk::wallet::{HttpClient, MultiMintWallet, Wallet}; use cdk_redb::WalletRedbDatabase; use cdk_sqlite::WalletSqliteDatabase; use clap::{Parser, Subcommand}; @@ -78,6 +77,10 @@ enum Commands { PayRequest(sub_commands::pay_request::PayRequestSubCommand), /// Create Payment request CreateRequest(sub_commands::create_request::CreateRequestSubCommand), + /// Mint blind auth proofs + MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand), + /// Cat login + CatLogin(sub_commands::cat_login::CatLoginSubCommand), } #[tokio::main] @@ -151,6 +154,7 @@ async fn main() -> Result<()> { localstore.clone(), &mnemonic.to_seed_normalized(""), None, + None, )?; if let Some(proxy_url) = args.proxy.as_ref() { let http_client = HttpClient::with_proxy(mint_url, proxy_url.clone(), None, true)?; @@ -228,5 +232,23 @@ async fn main() -> Result<()> { Commands::CreateRequest(sub_command_args) => { sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await } + Commands::MintBlindAuth(sub_command_args) => { + sub_commands::mint_blind_auth::mint_blind_auth( + &multi_mint_wallet, + &mnemonic.to_seed_normalized(""), + localstore, + sub_command_args, + ) + .await + } + Commands::CatLogin(sub_command_args) => { + sub_commands::cat_login::cat_login( + &multi_mint_wallet, + &mnemonic.to_seed_normalized(""), + localstore, + sub_command_args, + ) + .await + } } } diff --git a/crates/cdk-cli/src/sub_commands/cat_login.rs b/crates/cdk-cli/src/sub_commands/cat_login.rs new file mode 100644 index 000000000..d9bd4bfb9 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/cat_login.rs @@ -0,0 +1,70 @@ +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::Result; +use cdk::cdk_database::{Error, WalletDatabase}; +use cdk::mint_url::MintUrl; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::types::WalletKey; +use cdk::wallet::{MultiMintWallet, Wallet}; +use cdk::OidcClient; +use clap::Args; +use serde::{Deserialize, Serialize}; + +#[derive(Args, Serialize, Deserialize)] +pub struct CatLoginSubCommand { + /// Mint url + mint_url: MintUrl, + /// Username + username: String, + /// Password + password: String, + /// Currency unit e.g. sat + #[arg(default_value = "sat")] + #[arg(short, long)] + unit: String, +} + +pub async fn cat_login( + multi_mint_wallet: &MultiMintWallet, + seed: &[u8], + localstore: Arc + Sync + Send>, + sub_command_args: &CatLoginSubCommand, +) -> Result<()> { + let mint_url = sub_command_args.mint_url.clone(); + let unit = CurrencyUnit::from_str(&sub_command_args.unit)?; + + let wallet = match multi_mint_wallet + .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone())) + .await + { + Some(wallet) => wallet.clone(), + None => { + let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None, None)?; + + multi_mint_wallet.add_wallet(wallet.clone()).await; + wallet + } + }; + + let mint_info = wallet.get_mint_info().await?.expect("Mint info not found"); + + let openid_discovery = mint_info + .nuts + .nutxx + .expect("Nutxx defined") + .openid_discovery; + + let oidc_client = OidcClient::new(openid_discovery); + + let access_token = oidc_client + .get_access_token_with_user_password( + sub_command_args.username.clone(), + sub_command_args.password.clone(), + ) + .await?; + + println!("access_token: {}", access_token); + + Ok(()) +} diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index b438edf61..1a30be1dd 100644 --- a/crates/cdk-cli/src/sub_commands/mint.rs +++ b/crates/cdk-cli/src/sub_commands/mint.rs @@ -43,13 +43,15 @@ pub async fn mint( { Some(wallet) => wallet.clone(), None => { - let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None)?; + let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None, None)?; multi_mint_wallet.add_wallet(wallet.clone()).await; wallet } }; + wallet.get_mint_info().await?; + let quote = wallet .mint_quote(Amount::from(sub_command_args.amount), description) .await?; diff --git a/crates/cdk-cli/src/sub_commands/mint_blind_auth.rs b/crates/cdk-cli/src/sub_commands/mint_blind_auth.rs new file mode 100644 index 000000000..0d8ad0c25 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/mint_blind_auth.rs @@ -0,0 +1,68 @@ +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::Result; +use cdk::cdk_database::{Error, WalletDatabase}; +use cdk::mint_url::MintUrl; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::types::WalletKey; +use cdk::wallet::{MultiMintWallet, Wallet}; +use cdk::Amount; +use clap::Args; +use serde::{Deserialize, Serialize}; + +#[derive(Args, Serialize, Deserialize)] +pub struct MintBlindAuthSubCommand { + /// Mint url + mint_url: MintUrl, + /// Amount + amount: u64, + /// Cat + cat: String, + /// Currency unit e.g. sat + #[arg(default_value = "sat")] + #[arg(short, long)] + unit: String, +} + +pub async fn mint_blind_auth( + multi_mint_wallet: &MultiMintWallet, + seed: &[u8], + localstore: Arc + Sync + Send>, + sub_command_args: &MintBlindAuthSubCommand, +) -> Result<()> { + let mint_url = sub_command_args.mint_url.clone(); + let unit = CurrencyUnit::from_str(&sub_command_args.unit)?; + + let wallet = match multi_mint_wallet + .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone())) + .await + { + Some(wallet) => wallet.clone(), + None => { + let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None, None)?; + + multi_mint_wallet.add_wallet(wallet.clone()).await; + wallet + } + }; + + wallet.get_mint_info().await?; + + { + let mut cat = wallet.cat.write().await; + + *cat = Some(sub_command_args.cat.clone()); + } + + let proofs = wallet + .mint_blind_auth(Amount::from(sub_command_args.amount)) + .await?; + + println!( + "Received {} from auth proofs for mint {mint_url}", + proofs.len() + ); + + Ok(()) +} diff --git a/crates/cdk-cli/src/sub_commands/mint_info.rs b/crates/cdk-cli/src/sub_commands/mint_info.rs index b2dc0b1cb..793457faf 100644 --- a/crates/cdk-cli/src/sub_commands/mint_info.rs +++ b/crates/cdk-cli/src/sub_commands/mint_info.rs @@ -1,6 +1,6 @@ use anyhow::Result; use cdk::mint_url::MintUrl; -use cdk::wallet::client::MintConnector; +use cdk::wallet::MintConnector; use cdk::HttpClient; use clap::Args; use url::Url; diff --git a/crates/cdk-cli/src/sub_commands/mod.rs b/crates/cdk-cli/src/sub_commands/mod.rs index 8256d0aea..3c4a8e5f9 100644 --- a/crates/cdk-cli/src/sub_commands/mod.rs +++ b/crates/cdk-cli/src/sub_commands/mod.rs @@ -1,5 +1,6 @@ pub mod balance; pub mod burn; +pub mod cat_login; pub mod check_spent; pub mod create_request; pub mod decode_request; @@ -7,6 +8,7 @@ pub mod decode_token; pub mod list_mint_proofs; pub mod melt; pub mod mint; +pub mod mint_blind_auth; pub mod mint_info; pub mod pay_request; pub mod pending_mints; diff --git a/crates/cdk-cli/src/sub_commands/receive.rs b/crates/cdk-cli/src/sub_commands/receive.rs index 990ca4011..1372815e4 100644 --- a/crates/cdk-cli/src/sub_commands/receive.rs +++ b/crates/cdk-cli/src/sub_commands/receive.rs @@ -149,6 +149,7 @@ async fn receive_token( localstore, seed, None, + None, )?; multi_mint_wallet.add_wallet(wallet).await; } diff --git a/crates/cdk-cli/src/sub_commands/restore.rs b/crates/cdk-cli/src/sub_commands/restore.rs index 41d7839b1..b546f9eb8 100644 --- a/crates/cdk-cli/src/sub_commands/restore.rs +++ b/crates/cdk-cli/src/sub_commands/restore.rs @@ -33,7 +33,7 @@ pub async fn restore( { Some(wallet) => wallet.clone(), None => { - let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None)?; + let wallet = Wallet::new(&mint_url.to_string(), unit, localstore, seed, None, None)?; multi_mint_wallet.add_wallet(wallet.clone()).await; wallet diff --git a/crates/cdk-common/src/database/mint/auth/mod.rs b/crates/cdk-common/src/database/mint/auth/mod.rs new file mode 100644 index 000000000..e2c2a6950 --- /dev/null +++ b/crates/cdk-common/src/database/mint/auth/mod.rs @@ -0,0 +1,49 @@ +//! Mint in memory database use std::collections::HashMap; + +use async_trait::async_trait; + +use crate::database::Error; +use crate::mint::MintKeySetInfo; +use crate::nuts::nut07::State; +use crate::nuts::{AuthProof, BlindSignature, Id, PublicKey}; + +/// Mint Database trait +#[async_trait] +pub trait MintAuthDatabase { + /// Mint Database Error + type Err: Into + From; + /// Add Active Keyset + async fn set_active_keyset(&self, id: Id) -> Result<(), Self::Err>; + /// Get Active Keyset + async fn get_active_keyset_id(&self) -> Result, Self::Err>; + + /// Add [`MintKeySetInfo`] + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>; + /// Get [`MintKeySetInfo`] + async fn get_keyset_info(&self, id: &Id) -> Result, Self::Err>; + /// Get [`MintKeySetInfo`]s + async fn get_keyset_infos(&self) -> Result, Self::Err>; + + /// Add spent [`Proofs`] + async fn add_proof(&self, proof: AuthProof) -> Result<(), Self::Err>; + /// Get [`Proofs`] state + async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err>; + /// Get [`Proofs`] state + async fn update_proof_state( + &self, + y: &PublicKey, + proofs_state: State, + ) -> Result, Self::Err>; + + /// Add [`BlindSignature`] + async fn add_blind_signatures( + &self, + blinded_messages: &[PublicKey], + blind_signatures: &[BlindSignature], + ) -> Result<(), Self::Err>; + /// Get [`BlindSignature`]s + async fn get_blind_signatures( + &self, + blinded_messages: &[PublicKey], + ) -> Result>, Self::Err>; +} diff --git a/crates/cdk-common/src/database/mint.rs b/crates/cdk-common/src/database/mint/mod.rs similarity index 99% rename from crates/cdk-common/src/database/mint.rs rename to crates/cdk-common/src/database/mint/mod.rs index 4755b626d..7d1949cd7 100644 --- a/crates/cdk-common/src/database/mint.rs +++ b/crates/cdk-common/src/database/mint/mod.rs @@ -13,6 +13,10 @@ use crate::nuts::{ Proofs, PublicKey, State, }; +mod auth; + +pub use auth::MintAuthDatabase; + /// Mint Database trait #[async_trait] pub trait Database { diff --git a/crates/cdk-common/src/database/mod.rs b/crates/cdk-common/src/database/mod.rs index f9a0e5cac..40191f9c7 100644 --- a/crates/cdk-common/src/database/mod.rs +++ b/crates/cdk-common/src/database/mod.rs @@ -7,6 +7,8 @@ mod wallet; #[cfg(feature = "mint")] pub use mint::Database as MintDatabase; +#[cfg(feature = "mint")] +pub use mint::MintAuthDatabase; #[cfg(feature = "wallet")] pub use wallet::Database as WalletDatabase; @@ -25,6 +27,9 @@ pub enum Error { /// NUT02 Error #[error(transparent)] NUT02(#[from] crate::nuts::nut02::Error), + /// NUT00 Error + #[error(transparent)] + NUTXX1(#[from] crate::nuts::nutxx1::Error), /// Serde Error #[error(transparent)] Serde(#[from] serde_json::Error), diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index 6c2471c2e..b7cf7b579 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -51,6 +51,24 @@ pub enum Error { /// Amountless Invoice Not supported #[error("Amount Less Invoice is not allowed")] AmountLessNotAllowed, + /// Auth Required + #[error("Auth Required")] + AuthRequired, + /// Auth settings undefined + #[error("Auth settings undefinded")] + AuthSettingsUndefinded, + /// Mint time outside of tolerance + #[error("Mint time outside of tolerance")] + MintTimeExceedsTolerance, + /// Insufficient blind auth tokens + #[error("Insufficient blind auth tokens, must reauth")] + InsufficientBlindAuthTokens, + /// Auth localstore undefined + #[error("Auth localstore undefinded")] + AuthLocalstoreUndefinded, + /// Wallet cat not set + #[error("Wallet cat not set")] + CatNotSet, // Mint Errors /// Minting is disabled @@ -113,6 +131,9 @@ pub enum Error { /// Internal Error #[error("Internal Error")] Internal, + /// Oidc config not set + #[error("Oidc client not set")] + OidcNotSet, // Wallet Errors /// P2PK spending conditions not met @@ -200,7 +221,7 @@ pub enum Error { /// Http transport error #[error("Http transport error: {0}")] HttpError(String), - + #[cfg(feature = "wallet")] // Crate error conversions /// Cashu Url Error #[error(transparent)] @@ -248,6 +269,12 @@ pub enum Error { /// NUT20 Error #[error(transparent)] NUT20(#[from] crate::nuts::nut20::Error), + /// NUTXX Error + #[error(transparent)] + NUTXX(#[from] crate::nuts::nutxx::Error), + /// NUTXX1 Error + #[error(transparent)] + NUTXX1(#[from] crate::nuts::nutxx1::Error), /// Database Error #[error(transparent)] Database(#[from] crate::database::Error), diff --git a/crates/cdk-common/src/lib.rs b/crates/cdk-common/src/lib.rs index 52e7068eb..85d423bd5 100644 --- a/crates/cdk-common/src/lib.rs +++ b/crates/cdk-common/src/lib.rs @@ -15,7 +15,6 @@ pub mod pub_sub; #[cfg(feature = "mint")] pub mod subscription; pub mod ws; - // re-exporting external crates pub use bitcoin; pub use cashu::amount::{self, Amount}; @@ -26,3 +25,4 @@ pub use cashu::nuts::{self, *}; #[cfg(feature = "wallet")] pub use cashu::wallet; pub use cashu::{dhke, mint_url, secret, util, SECP256K1}; +pub use error::Error; diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index 85b0f09ba..7fcc42e24 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/crates/cdk-integration-tests/src/init_pure_tests.rs @@ -10,15 +10,15 @@ use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk::cdk_database::WalletMemoryDatabase; use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; use cdk::nuts::nut00::ProofsMethods; +use cdk::nuts::nutxx1::MintAuthRequest; use cdk::nuts::{ - CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse, + AuthToken, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, }; use cdk::util::unix_time; -use cdk::wallet::client::MintConnector; -use cdk::wallet::Wallet; +use cdk::wallet::{MintConnector, Wallet}; use cdk::{Amount, Error, Mint}; use cdk_fake_wallet::FakeWallet; use tokio::sync::Notify; @@ -52,27 +52,35 @@ impl Debug for DirectMintConnection { /// Convert the requests and responses between the [String] and [Uuid] variants as necessary. #[async_trait] impl MintConnector for DirectMintConnection { - async fn get_mint_keys(&self) -> Result, Error> { + async fn get_mint_keys(&self, _auth_token: Option) -> Result, Error> { self.mint.pubkeys().await.map(|pks| pks.keysets) } - async fn get_mint_keyset(&self, keyset_id: Id) -> Result { + async fn get_mint_keyset( + &self, + keyset_id: Id, + _auth_token: Option, + ) -> Result { self.mint .keyset(&keyset_id) .await .and_then(|res| res.ok_or(Error::UnknownKeySet)) } - async fn get_mint_keysets(&self) -> Result { + async fn get_mint_keysets( + &self, + _auth_token: Option, + ) -> Result { self.mint.keysets().await } async fn post_mint_quote( &self, request: MintQuoteBolt11Request, + auth_token: Option, ) -> Result, Error> { self.mint - .get_mint_bolt11_quote(request) + .get_mint_bolt11_quote(auth_token, request) .await .map(Into::into) } @@ -80,10 +88,11 @@ impl MintConnector for DirectMintConnection { async fn get_mint_quote_status( &self, quote_id: &str, + auth_token: Option, ) -> Result, Error> { let quote_id_uuid = Uuid::from_str(quote_id).unwrap(); self.mint - .check_mint_quote("e_id_uuid) + .check_mint_quote(auth_token, "e_id_uuid) .await .map(Into::into) } @@ -91,17 +100,21 @@ impl MintConnector for DirectMintConnection { async fn post_mint( &self, request: MintBolt11Request, + auth_token: Option, ) -> Result { let request_uuid = request.try_into().unwrap(); - self.mint.process_mint_request(request_uuid).await + self.mint + .process_mint_request(auth_token, request_uuid) + .await } async fn post_melt_quote( &self, request: MeltQuoteBolt11Request, + auth_token: Option, ) -> Result, Error> { self.mint - .get_melt_bolt11_quote(&request) + .get_melt_bolt11_quote(auth_token, &request) .await .map(Into::into) } @@ -109,10 +122,11 @@ impl MintConnector for DirectMintConnection { async fn get_melt_quote_status( &self, quote_id: &str, + auth_token: Option, ) -> Result, Error> { let quote_id_uuid = Uuid::from_str(quote_id).unwrap(); self.mint - .check_melt_quote("e_id_uuid) + .check_melt_quote(auth_token, "e_id_uuid) .await .map(Into::into) } @@ -120,13 +134,23 @@ impl MintConnector for DirectMintConnection { async fn post_melt( &self, request: MeltBolt11Request, + auth_token: Option, ) -> Result, Error> { let request_uuid = request.try_into().unwrap(); - self.mint.melt_bolt11(&request_uuid).await.map(Into::into) + self.mint + .melt_bolt11(auth_token, &request_uuid) + .await + .map(Into::into) } - async fn post_swap(&self, swap_request: SwapRequest) -> Result { - self.mint.process_swap_request(swap_request).await + async fn post_swap( + &self, + swap_request: SwapRequest, + auth_token: Option, + ) -> Result { + self.mint + .process_swap_request(auth_token, swap_request) + .await } async fn get_mint_info(&self) -> Result { @@ -136,12 +160,45 @@ impl MintConnector for DirectMintConnection { async fn post_check_state( &self, request: CheckStateRequest, + auth_token: Option, ) -> Result { - self.mint.check_state(&request).await + self.mint.check_state(auth_token, &request).await + } + + async fn post_restore( + &self, + request: RestoreRequest, + auth_token: Option, + ) -> Result { + self.mint.restore(auth_token, request).await + } + + /// Get Blind Auth keys + async fn get_mint_blind_auth_keys(&self) -> Result, Error> { + Ok(self.mint.auth_pubkeys().await?.keysets) } - async fn post_restore(&self, request: RestoreRequest) -> Result { - self.mint.restore(request).await + /// Get Blind Auth Keyset + async fn get_mint_blind_auth_keyset(&self, keyset_id: Id) -> Result { + Ok(self + .mint + .keyset(&keyset_id) + .await? + .ok_or(Error::UnknownKeySet)?) + } + + /// Get Blind Auth keysets + async fn get_mint_blind_auth_keysets(&self) -> Result { + self.mint.auth_keysets().await + } + + /// Post mint blind auth + async fn post_mint_blind_auth( + &self, + request: MintAuthRequest, + auth_token: AuthToken, + ) -> Result { + self.mint.mint_blind_auth(auth_token, request).await } } @@ -201,7 +258,7 @@ pub fn create_test_wallet_for_mint(mint: Arc) -> anyhow::Result Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -65,6 +65,7 @@ async fn test_fake_melt_payment_fail() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -128,6 +129,7 @@ async fn test_fake_melt_payment_fail_and_check() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -173,6 +175,7 @@ async fn test_fake_melt_payment_return_fail_status() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -233,6 +236,7 @@ async fn test_fake_melt_payment_error_unknown() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -294,6 +298,7 @@ async fn test_fake_melt_payment_err_paid() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -332,6 +337,7 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -362,7 +368,7 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { outputs: Some(premint_secrets.blinded_messages()), }; - let melt_response = client.post_melt(melt_request).await?; + let melt_response = client.post_melt(melt_request, None).await?; assert!(melt_response.change.is_some()); @@ -385,6 +391,7 @@ async fn test_fake_mint_with_witness() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -409,6 +416,7 @@ async fn test_fake_mint_without_witness() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -428,7 +436,7 @@ async fn test_fake_mint_without_witness() -> Result<()> { signature: None, }; - let response = http_client.post_mint(request.clone()).await; + let response = http_client.post_mint(request.clone(), None).await; match response { Err(cdk::error::Error::SignatureMissingOrInvalid) => Ok(()), @@ -437,7 +445,6 @@ async fn test_fake_mint_without_witness() -> Result<()> { } } -// TODO: Rewrite this test to include witness wrong #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_mint_with_wrong_witness() -> Result<()> { let wallet = Wallet::new( @@ -446,6 +453,7 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -469,7 +477,7 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> { request.sign(secret_key)?; - let response = http_client.post_mint(request.clone()).await; + let response = http_client.post_mint(request.clone(), None).await; match response { Err(cdk::error::Error::SignatureMissingOrInvalid) => Ok(()), diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs index 10cd5a164..582b2fd7c 100644 --- a/crates/cdk-integration-tests/tests/mint.rs +++ b/crates/cdk-integration-tests/tests/mint.rs @@ -51,9 +51,11 @@ async fn new_mint(fee: u64) -> Mint { mint_info, quote_ttl, Arc::new(MintMemoryDatabase::default()), + None, HashMap::new(), supported_units, HashMap::new(), + HashMap::new(), ) .await .unwrap() @@ -94,7 +96,7 @@ async fn mint_proofs( signature: None, }; - let after_mint = mint.process_mint_request(mint_request).await?; + let after_mint = mint.process_mint_request(None, mint_request).await?; let proofs = construct_proofs( after_mint.signatures, @@ -119,7 +121,7 @@ async fn test_mint_double_spend() -> Result<()> { let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - let swap = mint.process_swap_request(swap_request).await; + let swap = mint.process_swap_request(None, swap_request).await; assert!(swap.is_ok()); @@ -127,7 +129,7 @@ async fn test_mint_double_spend() -> Result<()> { let swap_two_request = SwapRequest::new(proofs, preswap_two.blinded_messages()); - match mint.process_swap_request(swap_two_request).await { + match mint.process_swap_request(None, swap_two_request).await { Ok(_) => bail!("Proofs double spent"), Err(err) => match err { cdk::Error::TokenAlreadySpent => (), @@ -163,7 +165,7 @@ async fn test_attempt_to_swap_by_overflowing() -> Result<()> { let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages()); - match mint.process_swap_request(swap_request).await { + match mint.process_swap_request(None, swap_request).await { Ok(_) => bail!("Swap occurred with overflow"), Err(err) => match err { cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)) => (), @@ -201,7 +203,7 @@ pub async fn test_p2pk_swap() -> Result<()> { let keys = mint.pubkeys().await?.keysets.first().cloned().unwrap().keys; - let post_swap = mint.process_swap_request(swap_request).await?; + let post_swap = mint.process_swap_request(None, swap_request).await?; let mut proofs = construct_proofs( post_swap.signatures, @@ -243,7 +245,7 @@ pub async fn test_p2pk_swap() -> Result<()> { .await .expect("valid subscription"); - match mint.process_swap_request(swap_request).await { + match mint.process_swap_request(None, swap_request).await { Ok(_) => bail!("Proofs spent without sig"), Err(err) => match err { cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided) => (), @@ -260,7 +262,7 @@ pub async fn test_p2pk_swap() -> Result<()> { let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages()); - let attempt_swap = mint.process_swap_request(swap_request).await; + let attempt_swap = mint.process_swap_request(None, swap_request).await; assert!(attempt_swap.is_ok()); @@ -308,7 +310,7 @@ async fn test_swap_unbalanced() -> Result<()> { let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - match mint.process_swap_request(swap_request).await { + match mint.process_swap_request(None, swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), @@ -320,7 +322,7 @@ async fn test_swap_unbalanced() -> Result<()> { let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); - match mint.process_swap_request(swap_request).await { + match mint.process_swap_request(None, swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), @@ -348,7 +350,7 @@ async fn test_swap_overpay_underpay_fee() -> Result<()> { let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); // Attempt to swap overpaying fee - match mint.process_swap_request(swap_request).await { + match mint.process_swap_request(None, swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), @@ -364,7 +366,7 @@ async fn test_swap_overpay_underpay_fee() -> Result<()> { let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); // Attempt to swap underpaying fee - match mint.process_swap_request(swap_request).await { + match mint.process_swap_request(None, swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), @@ -394,7 +396,7 @@ async fn test_mint_enforce_fee() -> Result<()> { let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages()); // Attempt to swap underpaying fee - match mint.process_swap_request(swap_request).await { + match mint.process_swap_request(None, swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), @@ -409,7 +411,7 @@ async fn test_mint_enforce_fee() -> Result<()> { let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages()); - let _ = mint.process_swap_request(swap_request).await?; + let _ = mint.process_swap_request(None, swap_request).await?; let thousnad_proofs: Vec<_> = proofs.drain(..1001).collect(); @@ -418,7 +420,7 @@ async fn test_mint_enforce_fee() -> Result<()> { let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages()); // Attempt to swap underpaying fee - match mint.process_swap_request(swap_request).await { + match mint.process_swap_request(None, swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), @@ -433,7 +435,7 @@ async fn test_mint_enforce_fee() -> Result<()> { let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages()); - let _ = mint.process_swap_request(swap_request).await?; + let _ = mint.process_swap_request(None, swap_request).await?; Ok(()) } diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 1f31f6cd3..bcab3b1da 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -12,8 +12,7 @@ use cdk::nuts::{ CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload, PreMintSecrets, State, }; -use cdk::wallet::client::{HttpClient, MintConnector}; -use cdk::wallet::{Wallet, WalletSubscription}; +use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription}; use cdk_integration_tests::init_regtest::{ get_mint_url, get_mint_ws_url, init_cln_client, init_lnd_client, }; @@ -68,6 +67,7 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let (ws_stream, _) = connect_async(get_mint_ws_url()) @@ -151,6 +151,7 @@ async fn test_regtest_mint_melt() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_amount = Amount::from(100); @@ -183,6 +184,7 @@ async fn test_restore() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &seed, None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -201,6 +203,7 @@ async fn test_restore() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &seed, None, + None, )?; assert!(wallet_2.total_balance().await? == 0.into()); @@ -239,6 +242,7 @@ async fn test_pay_invoice_twice() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &seed, None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -293,6 +297,7 @@ async fn test_internal_payment() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &seed, None, + None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; @@ -313,6 +318,7 @@ async fn test_internal_payment() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &seed, None, + None, )?; let mint_quote = wallet_2.mint_quote(10.into(), None).await?; @@ -362,6 +368,7 @@ async fn test_cached_mint() -> Result<()> { Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, + None, )?; let mint_amount = Amount::from(100); @@ -398,8 +405,8 @@ async fn test_cached_mint() -> Result<()> { request.sign(secret_key.expect("Secret key on quote"))?; - let response = http_client.post_mint(request.clone()).await?; - let response1 = http_client.post_mint(request).await?; + let response = http_client.post_mint(request.clone(), None).await?; + let response1 = http_client.post_mint(request, None).await?; assert!(response == response1); Ok(()) diff --git a/crates/cdk-mintd/src/config.rs b/crates/cdk-mintd/src/config.rs index df46c279b..4525940d9 100644 --- a/crates/cdk-mintd/src/config.rs +++ b/crates/cdk-mintd/src/config.rs @@ -174,6 +174,16 @@ pub struct Database { pub engine: DatabaseEngine, } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Auth { + pub openid_discovery: String, + pub openid_client_id: String, + pub mint_max_bat: u64, + pub enabled_mint: bool, + pub enabled_melt: bool, + pub enabled_swap: bool, +} + /// CDK settings, derived from `config.toml` #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Settings { @@ -187,6 +197,7 @@ pub struct Settings { pub lnd: Option, pub fake_wallet: Option, pub database: Database, + pub auth: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/crates/cdk-mintd/src/main.rs b/crates/cdk-mintd/src/main.rs index ca2d3f7df..2b1c08e43 100644 --- a/crates/cdk-mintd/src/main.rs +++ b/crates/cdk-mintd/src/main.rs @@ -15,20 +15,25 @@ use axum::middleware::Next; use axum::response::Response; use axum::{middleware, Router}; use bip39::Mnemonic; -use cdk::cdk_database::{self, MintDatabase}; +use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase}; use cdk::cdk_lightning; use cdk::cdk_lightning::MintLightning; use cdk::mint::{MintBuilder, MintMeltLimits}; use cdk::nuts::nut17::SupportedMethods; use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path}; -use cdk::nuts::{ContactInfo, CurrencyUnit, MintVersion, PaymentMethod}; +use cdk::nuts::{ + AuthRequired, ContactInfo, CurrencyUnit, MintVersion, PaymentMethod, ProtectedEndpoint, + RoutePath, +}; use cdk::types::LnKey; use cdk_axum::cache::HttpCache; use cdk_mintd::cli::CLIArgs; use cdk_mintd::config::{self, DatabaseEngine, LnBackend}; use cdk_mintd::env_vars::ENV_WORK_DIR; use cdk_mintd::setup::LnBackendSetup; +use cdk_redb::mint::MintRedbAuthDatabase; use cdk_redb::MintRedbDatabase; +use cdk_sqlite::mint::MintSqliteAuthDatabase; use cdk_sqlite::MintSqliteDatabase; use clap::Parser; use tokio::sync::Notify; @@ -317,6 +322,82 @@ async fn main() -> anyhow::Result<()> { mint_builder = mint_builder.add_cache(Some(cache.ttl.as_secs()), cached_endpoints); + // Add auth to mint + if let Some(auth_settings) = settings.auth { + let mint_blind_auth_endpoint = + ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintBlindAuth); + + mint_builder = mint_builder.set_cleat_auth_settings( + auth_settings.openid_discovery, + auth_settings.openid_client_id, + vec![mint_blind_auth_endpoint], + ); + + let mut protected_endpoints = HashMap::new(); + + let mut blind_auth_endpoints = vec![]; + + if auth_settings.enabled_mint { + let mint_quote_protected_endpoint = ProtectedEndpoint::new( + cdk::nuts::Method::Post, + cdk::nuts::RoutePath::MintQuoteBolt11, + ); + protected_endpoints.insert(mint_quote_protected_endpoint, AuthRequired::Blind); + let mint_protected_endpoint = + ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::MintBolt11); + + protected_endpoints.insert(mint_protected_endpoint, AuthRequired::Blind); + + blind_auth_endpoints.push(mint_quote_protected_endpoint); + blind_auth_endpoints.push(mint_protected_endpoint); + } + + if auth_settings.enabled_melt { + let melt_quote_protected_endpoint = ProtectedEndpoint::new( + cdk::nuts::Method::Post, + cdk::nuts::RoutePath::MeltQuoteBolt11, + ); + + protected_endpoints.insert(melt_quote_protected_endpoint, AuthRequired::Blind); + + let melt_protected_endpoint = + ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::MeltBolt11); + protected_endpoints.insert(melt_protected_endpoint, AuthRequired::Blind); + + blind_auth_endpoints.push(melt_quote_protected_endpoint); + blind_auth_endpoints.push(melt_protected_endpoint); + } + + if auth_settings.enabled_swap { + let swap_protected_endpoint = + ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::Swap); + protected_endpoints.insert(swap_protected_endpoint, AuthRequired::Blind); + + blind_auth_endpoints.push(swap_protected_endpoint); + } + + mint_builder = + mint_builder.set_blind_auth_settings(auth_settings.mint_max_bat, blind_auth_endpoints); + + let auth_localstore: Arc + Send + Sync> = + match settings.database.engine { + DatabaseEngine::Sqlite => { + let sql_db_path = work_dir.join("cdk-mintd-auth.sqlite"); + let sqlite_db = MintSqliteAuthDatabase::new(&sql_db_path).await?; + + sqlite_db.migrate().await; + + Arc::new(sqlite_db) + } + DatabaseEngine::Redb => { + let redb_path = work_dir.join("cdk-mintd-auth.redb"); + Arc::new(MintRedbAuthDatabase::new(&redb_path)?) + } + }; + + mint_builder = mint_builder.with_auth_localstore(auth_localstore); + } + let mint = mint_builder.build().await?; let mint = Arc::new(mint); diff --git a/crates/cdk-redb/Cargo.toml b/crates/cdk-redb/Cargo.toml index 06ed9a828..b5435d808 100644 --- a/crates/cdk-redb/Cargo.toml +++ b/crates/cdk-redb/Cargo.toml @@ -17,6 +17,7 @@ wallet = [] [dependencies] async-trait = "0.1" +cashu = { path = "../cashu", version = "0.6.0" } cdk-common = { path = "../cdk-common", version = "0.6.0" } redb = "2.1.0" thiserror = "1" diff --git a/crates/cdk-redb/src/mint/auth/mod.rs b/crates/cdk-redb/src/mint/auth/mod.rs new file mode 100644 index 000000000..ea3892d96 --- /dev/null +++ b/crates/cdk-redb/src/mint/auth/mod.rs @@ -0,0 +1,304 @@ +use std::cmp::Ordering; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; + +use async_trait::async_trait; +use cashu::dhke::hash_to_curve; +use cashu::nuts::{AuthProof, BlindSignature, Id, PublicKey, State}; +use cdk_common::database::{self, MintAuthDatabase}; +use cdk_common::mint::MintKeySetInfo; +use redb::{Database, ReadableTable, TableDefinition}; + +use crate::error::Error; + +const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config"); +const ACTIVE_KEYSET_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keyset"); +const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets"); +const PROOFS_TABLE: TableDefinition<[u8; 33], &str> = TableDefinition::new("proofs"); +const PROOFS_STATE_TABLE: TableDefinition<[u8; 33], &str> = TableDefinition::new("proofs_state"); +// Key is hex blinded_message B_ value is blinded_signature +const BLINDED_SIGNATURES: TableDefinition<[u8; 33], &str> = + TableDefinition::new("blinded_signatures"); + +/// Mint Redbdatabase +#[derive(Debug, Clone)] +pub struct MintRedbAuthDatabase { + db: Arc, +} + +const DATABASE_VERSION: u32 = 0; + +impl MintRedbAuthDatabase { + /// Create new [`MintRedbDatabase`] + pub fn new(path: &Path) -> Result { + { + // Check database version + + let db = Arc::new(Database::create(path)?); + + // Check database version + let read_txn = db.begin_read()?; + let table = read_txn.open_table(CONFIG_TABLE); + + let db_version = match table { + Ok(table) => table.get("db_version")?.map(|v| v.value().to_owned()), + Err(_) => None, + }; + match db_version { + Some(db_version) => { + let current_file_version = u32::from_str(&db_version)?; + match current_file_version.cmp(&DATABASE_VERSION) { + Ordering::Less => { + tracing::info!( + "Database needs to be upgraded at {} current is {}", + current_file_version, + DATABASE_VERSION + ); + } + Ordering::Equal => { + tracing::info!("Database is at current version {}", DATABASE_VERSION); + } + Ordering::Greater => { + tracing::warn!( + "Database upgrade did not complete at {} current is {}", + current_file_version, + DATABASE_VERSION + ); + return Err(Error::UnknownDatabaseVersion); + } + } + } + None => { + let write_txn = db.begin_write()?; + { + let mut table = write_txn.open_table(CONFIG_TABLE)?; + // Open all tables to init a new db + let _ = write_txn.open_table(ACTIVE_KEYSET_TABLE)?; + let _ = write_txn.open_table(KEYSETS_TABLE)?; + let _ = write_txn.open_table(PROOFS_TABLE)?; + let _ = write_txn.open_table(PROOFS_STATE_TABLE)?; + let _ = write_txn.open_table(BLINDED_SIGNATURES)?; + + table.insert("db_version", DATABASE_VERSION.to_string().as_str())?; + } + + write_txn.commit()?; + } + } + drop(db); + } + + let db = Database::create(path)?; + Ok(Self { db: Arc::new(db) }) + } +} + +#[async_trait] +impl MintAuthDatabase for MintRedbAuthDatabase { + type Err = database::Error; + + async fn set_active_keyset(&self, id: Id) -> Result<(), Self::Err> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + + { + let mut table = write_txn + .open_table(ACTIVE_KEYSET_TABLE) + .map_err(Error::from)?; + table + .insert("active_keyset_id", id.to_string().as_str()) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; + + Ok(()) + } + + async fn get_active_keyset_id(&self) -> Result, Self::Err> { + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn + .open_table(ACTIVE_KEYSET_TABLE) + .map_err(Error::from)?; + + if let Some(id) = table.get("active_keyset_id").map_err(Error::from)? { + return Ok(Some(Id::from_str(id.value()).map_err(Error::from)?)); + } + + Ok(None) + } + + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + + { + let mut table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; + table + .insert( + keyset.id.to_string().as_str(), + serde_json::to_string(&keyset) + .map_err(Error::from)? + .as_str(), + ) + .map_err(Error::from)?; + } + write_txn.commit().map_err(Error::from)?; + + Ok(()) + } + + async fn get_keyset_info(&self, keyset_id: &Id) -> Result, Self::Err> { + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; + + match table + .get(keyset_id.to_string().as_str()) + .map_err(Error::from)? + { + Some(keyset) => Ok(serde_json::from_str(keyset.value()).map_err(Error::from)?), + None => Ok(None), + } + } + + async fn get_keyset_infos(&self) -> Result, Self::Err> { + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?; + + let mut keysets = Vec::new(); + + for (_id, keyset) in (table.iter().map_err(Error::from)?).flatten() { + let keyset = serde_json::from_str(keyset.value()).map_err(Error::from)?; + + keysets.push(keyset) + } + + Ok(keysets) + } + + async fn add_proof(&self, proof: AuthProof) -> Result<(), Self::Err> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + + { + let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?; + let y: PublicKey = hash_to_curve(&proof.secret.to_bytes()).map_err(Error::from)?; + let y = y.to_bytes(); + if table.get(y).map_err(Error::from)?.is_none() { + table + .insert( + y, + serde_json::to_string(&proof).map_err(Error::from)?.as_str(), + ) + .map_err(Error::from)?; + } + } + write_txn.commit().map_err(Error::from)?; + + Ok(()) + } + + async fn update_proof_state( + &self, + y: &PublicKey, + proof_state: State, + ) -> Result, Self::Err> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + + let state_str = serde_json::to_string(&proof_state).map_err(Error::from)?; + + let current_state; + + { + let mut table = write_txn + .open_table(PROOFS_STATE_TABLE) + .map_err(Error::from)?; + + { + match table.get(y.to_bytes()).map_err(Error::from)? { + Some(state) => { + current_state = + Some(serde_json::from_str(state.value()).map_err(Error::from)?) + } + None => current_state = None, + } + } + + if current_state != Some(State::Spent) { + table + .insert(y.to_bytes(), state_str.as_str()) + .map_err(Error::from)?; + } + } + + write_txn.commit().map_err(Error::from)?; + + Ok(current_state) + } + + async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err> { + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn + .open_table(PROOFS_STATE_TABLE) + .map_err(Error::from)?; + + let mut states = Vec::with_capacity(ys.len()); + + for y in ys { + match table.get(y.to_bytes()).map_err(Error::from)? { + Some(state) => states.push(Some( + serde_json::from_str(state.value()).map_err(Error::from)?, + )), + None => states.push(None), + } + } + + Ok(states) + } + + async fn add_blind_signatures( + &self, + blinded_messages: &[PublicKey], + blind_signatures: &[BlindSignature], + ) -> Result<(), Self::Err> { + let write_txn = self.db.begin_write().map_err(Error::from)?; + + { + let mut table = write_txn + .open_table(BLINDED_SIGNATURES) + .map_err(Error::from)?; + + for (blinded_message, blind_signature) in blinded_messages.iter().zip(blind_signatures) + { + let blind_sig = serde_json::to_string(&blind_signature).map_err(Error::from)?; + table + .insert(blinded_message.to_bytes(), blind_sig.as_str()) + .map_err(Error::from)?; + } + } + + write_txn.commit().map_err(Error::from)?; + + Ok(()) + } + + async fn get_blind_signatures( + &self, + blinded_messages: &[PublicKey], + ) -> Result>, Self::Err> { + let read_txn = self.db.begin_read().map_err(Error::from)?; + let table = read_txn + .open_table(BLINDED_SIGNATURES) + .map_err(Error::from)?; + + let mut signatures = Vec::with_capacity(blinded_messages.len()); + + for blinded_message in blinded_messages { + match table.get(blinded_message.to_bytes()).map_err(Error::from)? { + Some(blind_signature) => signatures.push(Some( + serde_json::from_str(blind_signature.value()).map_err(Error::from)?, + )), + None => signatures.push(None), + } + } + + Ok(signatures) + } +} diff --git a/crates/cdk-redb/src/mint/mod.rs b/crates/cdk-redb/src/mint/mod.rs index 89385f45b..ffb639d58 100644 --- a/crates/cdk-redb/src/mint/mod.rs +++ b/crates/cdk-redb/src/mint/mod.rs @@ -24,8 +24,11 @@ use super::error::Error; use crate::migrations::migrate_00_to_01; use crate::mint::migrations::{migrate_02_to_03, migrate_03_to_04}; +mod auth; mod migrations; +pub use auth::MintRedbAuthDatabase; + const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets"); const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets"); const MINT_QUOTES_TABLE: TableDefinition<[u8; 16], &str> = TableDefinition::new("mint_quotes"); @@ -133,8 +136,8 @@ impl MintRedbDatabase { None => { let write_txn = db.begin_write()?; { - let mut table = write_txn.open_table(CONFIG_TABLE)?; // Open all tables to init a new db + let mut table = write_txn.open_table(CONFIG_TABLE)?; let _ = write_txn.open_table(ACTIVE_KEYSETS_TABLE)?; let _ = write_txn.open_table(KEYSETS_TABLE)?; let _ = write_txn.open_table(MINT_QUOTES_TABLE)?; diff --git a/crates/cdk-sqlite/Cargo.toml b/crates/cdk-sqlite/Cargo.toml index df0523d39..64b55d7ba 100644 --- a/crates/cdk-sqlite/Cargo.toml +++ b/crates/cdk-sqlite/Cargo.toml @@ -17,6 +17,7 @@ wallet = [] [dependencies] async-trait = "0.1" +cashu = { path = "../cashu", version = "0.6.0" } cdk-common = { path = "../cdk-common", version = "0.6.0" } bitcoin = { version = "0.32.2", default-features = false } sqlx = { version = "0.6.3", default-features = false, features = [ diff --git a/crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql b/crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql new file mode 100644 index 000000000..23664ad72 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/auth/migrations/20250109143347_init.sql @@ -0,0 +1,37 @@ +CREATE TABLE IF NOT EXISTS proof ( +y BLOB PRIMARY KEY, +keyset_id TEXT NOT NULL, +secret TEXT NOT NULL, +c BLOB NOT NULL, +state TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS state_index ON proof(state); +CREATE INDEX IF NOT EXISTS secret_index ON proof(secret); + + +-- Keysets Table + +CREATE TABLE IF NOT EXISTS keyset ( + id TEXT PRIMARY KEY, + unit TEXT NOT NULL, + active BOOL NOT NULL, + valid_from INTEGER NOT NULL, + valid_to INTEGER, + derivation_path TEXT NOT NULL, + max_order INTEGER NOT NULL, + derivation_path_index INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS unit_index ON keyset(unit); +CREATE INDEX IF NOT EXISTS active_index ON keyset(active); + + +CREATE TABLE IF NOT EXISTS blind_signature ( + y BLOB PRIMARY KEY, + amount INTEGER NOT NULL, + keyset_id TEXT NOT NULL, + c BLOB NOT NULL +); + +CREATE INDEX IF NOT EXISTS keyset_id_index ON blind_signature(keyset_id); diff --git a/crates/cdk-sqlite/src/mint/auth/mod.rs b/crates/cdk-sqlite/src/mint/auth/mod.rs new file mode 100644 index 000000000..06698fe6d --- /dev/null +++ b/crates/cdk-sqlite/src/mint/auth/mod.rs @@ -0,0 +1,392 @@ +//! SQLite Mint Auth + +use std::collections::HashMap; +use std::path::Path; +use std::str::FromStr; +use std::time::Duration; + +use async_trait::async_trait; +use cashu::nuts::{AuthProof, BlindSignature, Id, PublicKey, State}; +use cdk_common::database::{self, MintAuthDatabase}; +use cdk_common::mint::MintKeySetInfo; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; +use sqlx::Row; + +use super::{sqlite_row_to_blind_signature, sqlite_row_to_keyset_info}; +use crate::mint::Error; + +/// Mint SQLite Database +#[derive(Debug, Clone)] +pub struct MintSqliteAuthDatabase { + pool: SqlitePool, +} + +impl MintSqliteAuthDatabase { + /// Create new [`MintSqliteDatabase`] + pub async fn new(path: &Path) -> Result { + let path = path.to_str().ok_or(Error::InvalidDbPath)?; + let db_options = SqliteConnectOptions::from_str(path)? + .busy_timeout(Duration::from_secs(5)) + .read_only(false) + .create_if_missing(true) + .auto_vacuum(sqlx::sqlite::SqliteAutoVacuum::Full); + + let pool = SqlitePoolOptions::new() + .max_connections(1) + .connect_with(db_options) + .await?; + + Ok(Self { pool }) + } + + /// Migrate [`MintSqliteDatabase`] + pub async fn migrate(&self) { + sqlx::migrate!("./src/mint/auth/migrations") + .run(&self.pool) + .await + .expect("Could not run migrations"); + } +} + +#[async_trait] +impl MintAuthDatabase for MintSqliteAuthDatabase { + type Err = database::Error; + + async fn set_active_keyset(&self, id: Id) -> Result<(), Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let update_res = sqlx::query( + r#" + UPDATE keyset + SET active = CASE + WHEN id = ? THEN TRUE + ELSE FALSE + END; + "#, + ) + .bind(id.to_string()) + .execute(&mut transaction) + .await; + + match update_res { + Ok(_) => { + transaction.commit().await.map_err(Error::from)?; + Ok(()) + } + Err(err) => { + tracing::error!("SQLite Could not update keyset"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + Err(Error::from(err).into()) + } + } + } + + async fn get_active_keyset_id(&self) -> Result, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + + let rec = sqlx::query( + r#" +SELECT id +FROM keyset +WHERE active = 1; + "#, + ) + .fetch_one(&mut transaction) + .await; + + let rec = match rec { + Ok(rec) => { + transaction.commit().await.map_err(Error::from)?; + rec + } + Err(err) => match err { + sqlx::Error::RowNotFound => { + transaction.commit().await.map_err(Error::from)?; + return Ok(None); + } + _ => { + return { + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + Err(Error::SQLX(err).into()) + } + } + }, + }; + + Ok(Some( + Id::from_str(rec.try_get("id").map_err(Error::from)?).map_err(Error::from)?, + )) + } + + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let res = sqlx::query( + r#" +INSERT OR REPLACE INTO keyset +(id, unit, active, valid_from, valid_to, derivation_path, max_order, derivation_path_index) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); + "#, + ) + .bind(keyset.id.to_string()) + .bind(keyset.unit.to_string()) + .bind(keyset.active) + .bind(keyset.valid_from as i64) + .bind(keyset.valid_to.map(|v| v as i64)) + .bind(keyset.derivation_path.to_string()) + .bind(keyset.max_order) + .bind(keyset.derivation_path_index) + .execute(&mut transaction) + .await; + + match res { + Ok(_) => { + transaction.commit().await.map_err(Error::from)?; + Ok(()) + } + Err(err) => { + tracing::error!("SQLite could not add keyset info"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + + Err(Error::from(err).into()) + } + } + } + + async fn get_keyset_info(&self, id: &Id) -> Result, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let rec = sqlx::query( + r#" +SELECT * +FROM keyset +WHERE id=?; + "#, + ) + .bind(id.to_string()) + .fetch_one(&mut transaction) + .await; + + match rec { + Ok(rec) => { + transaction.commit().await.map_err(Error::from)?; + Ok(Some(sqlite_row_to_keyset_info(rec)?)) + } + Err(err) => match err { + sqlx::Error::RowNotFound => { + transaction.commit().await.map_err(Error::from)?; + return Ok(None); + } + _ => { + tracing::error!("SQLite could not get keyset info"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + return Err(Error::SQLX(err).into()); + } + }, + } + } + + async fn get_keyset_infos(&self) -> Result, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + let recs = sqlx::query( + r#" +SELECT * +FROM keyset; + "#, + ) + .fetch_all(&mut transaction) + .await + .map_err(Error::from); + + match recs { + Ok(recs) => { + transaction.commit().await.map_err(Error::from)?; + Ok(recs + .into_iter() + .map(sqlite_row_to_keyset_info) + .collect::>()?) + } + Err(err) => { + tracing::error!("SQLite could not get keyset info"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + Err(err.into()) + } + } + } + + async fn add_proof(&self, proof: AuthProof) -> Result<(), Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + if let Err(err) = sqlx::query( + r#" +INSERT INTO proof +(y, keyset_id, secret, c, state) +VALUES (?, ?, ?, ?, ?, ?); + "#, + ) + .bind(proof.y()?.to_bytes().to_vec()) + .bind(proof.keyset_id.to_string()) + .bind(proof.secret.to_string()) + .bind(proof.c.to_bytes().to_vec()) + .bind("UNSPENT") + .execute(&mut transaction) + .await + .map_err(Error::from) + { + tracing::debug!("Attempting to add known proof. Skipping.... {:?}", err); + } + transaction.commit().await.map_err(Error::from)?; + + Ok(()) + } + + async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + + let sql = format!( + "SELECT y, state FROM proof WHERE y IN ({})", + "?,".repeat(ys.len()).trim_end_matches(',') + ); + + let mut current_states = ys + .iter() + .fold(sqlx::query(&sql), |query, y| { + query.bind(y.to_bytes().to_vec()) + }) + .fetch_all(&mut transaction) + .await + .map_err(|err| { + tracing::error!("SQLite could not get state of proof: {err:?}"); + Error::SQLX(err) + })? + .into_iter() + .map(|row| { + PublicKey::from_slice(row.get("y")) + .map_err(Error::from) + .and_then(|y| { + let state: String = row.get("state"); + State::from_str(&state) + .map_err(Error::from) + .map(|state| (y, state)) + }) + }) + .collect::, _>>()?; + + Ok(ys.iter().map(|y| current_states.remove(y)).collect()) + } + + async fn update_proof_state( + &self, + y: &PublicKey, + proofs_state: State, + ) -> Result, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + + // Get current state for single y + let current_state = sqlx::query("SELECT state FROM proof WHERE y = ?") + .bind(y.to_bytes().to_vec()) + .fetch_optional(&mut transaction) + .await + .map_err(|err| { + tracing::error!("SQLite could not get state of proof: {err:?}"); + Error::SQLX(err) + })? + .map(|row| { + let state: String = row.get("state"); + State::from_str(&state).map_err(Error::from) + }) + .transpose()?; + + // Update state for single y + sqlx::query("UPDATE proof SET state = ? WHERE state != ? AND y = ?") + .bind(proofs_state.to_string()) + .bind(State::Spent.to_string()) + .bind(y.to_bytes().to_vec()) + .execute(&mut transaction) + .await + .map_err(|err| { + tracing::error!("SQLite could not update proof state: {err:?}"); + Error::SQLX(err) + })?; + + transaction.commit().await.map_err(Error::from)?; + Ok(current_state) + } + + async fn add_blind_signatures( + &self, + blinded_messages: &[PublicKey], + blind_signatures: &[BlindSignature], + ) -> Result<(), Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + for (message, signature) in blinded_messages.iter().zip(blind_signatures) { + let res = sqlx::query( + r#" +INSERT INTO blind_signature +(y, amount, keyset_id, c) +VALUES (?, ?, ?, ?, ?, ?); + "#, + ) + .bind(message.to_bytes().to_vec()) + .bind(u64::from(signature.amount) as i64) + .bind(signature.keyset_id.to_string()) + .bind(signature.c.to_bytes().to_vec()) + .execute(&mut transaction) + .await; + + if let Err(err) = res { + tracing::error!("SQLite could not add blind signature"); + if let Err(err) = transaction.rollback().await { + tracing::error!("Could not rollback sql transaction: {}", err); + } + return Err(Error::SQLX(err).into()); + } + } + + transaction.commit().await.map_err(Error::from)?; + + Ok(()) + } + + async fn get_blind_signatures( + &self, + blinded_messages: &[PublicKey], + ) -> Result>, Self::Err> { + let mut transaction = self.pool.begin().await.map_err(Error::from)?; + + let sql = format!( + "SELECT * FROM blind_signature WHERE y IN ({})", + "?,".repeat(blinded_messages.len()).trim_end_matches(',') + ); + + let mut blinded_signatures = blinded_messages + .iter() + .fold(sqlx::query(&sql), |query, y| { + query.bind(y.to_bytes().to_vec()) + }) + .fetch_all(&mut transaction) + .await + .map_err(|err| { + tracing::error!("SQLite could not get state of proof: {err:?}"); + Error::SQLX(err) + })? + .into_iter() + .map(|row| { + PublicKey::from_slice(row.get("y")) + .map_err(Error::from) + .and_then(|y| sqlite_row_to_blind_signature(row).map(|blinded| (y, blinded))) + }) + .collect::, _>>()?; + + Ok(blinded_messages + .iter() + .map(|y| blinded_signatures.remove(y)) + .collect()) + } +} diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index d0c6dbe0e..f3bc7e997 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -25,8 +25,11 @@ use sqlx::Row; use uuid::fmt::Hyphenated; use uuid::Uuid; +mod auth; pub mod error; +pub use auth::MintSqliteAuthDatabase; + /// Mint SQLite Database #[derive(Debug, Clone)] pub struct MintSqliteDatabase { @@ -1231,7 +1234,7 @@ fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result { let row_valid_to: Option = row.try_get("valid_to").map_err(Error::from)?; let row_derivation_path: String = row.try_get("derivation_path").map_err(Error::from)?; let row_max_order: u8 = row.try_get("max_order").map_err(Error::from)?; - let row_keyset_ppk: Option = row.try_get("input_fee_ppk").map_err(Error::from)?; + let row_keyset_ppk: Option = row.try_get("input_fee_ppk").ok(); let row_derivation_path_index: Option = row.try_get("derivation_path_index").map_err(Error::from)?; diff --git a/crates/cdk/Cargo.toml b/crates/cdk/Cargo.toml index 35eff847c..a1a2cc9b7 100644 --- a/crates/cdk/Cargo.toml +++ b/crates/cdk/Cargo.toml @@ -12,10 +12,10 @@ license = "MIT" [features] default = ["mint", "wallet"] -mint = ["dep:futures", "cdk-common/mint"] +wallet = ["dep:reqwest", "cdk-common/wallet"] +mint = ["dep:futures", "dep:reqwest", "cdk-common/mint"] # We do not commit to a MSRV with swagger enabled swagger = ["mint", "dep:utoipa", "cdk-common/swagger"] -wallet = ["dep:reqwest", "cdk-common/wallet"] bench = [] http_subscription = [] @@ -59,6 +59,8 @@ futures = { version = "0.3.28", default-features = false, optional = true, featu url = "2.3" utoipa = { version = "4", optional = true } uuid = { version = "1", features = ["v4", "serde"] } +# TODO: Put behind auth feature +jsonwebtoken = "9" # -Z minimal-versions sync_wrapper = "0.1.2" diff --git a/crates/cdk/README.md b/crates/cdk/README.md index cc8433518..a56ba3cc1 100644 --- a/crates/cdk/README.md +++ b/crates/cdk/README.md @@ -44,7 +44,7 @@ async fn main() { let localstore = WalletMemoryDatabase::default(); - let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed); + let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None); let quote = wallet.mint_quote(amount).await.unwrap(); diff --git a/crates/cdk/examples/mint-token.rs b/crates/cdk/examples/mint-token.rs index 9b9451929..7a56432b5 100644 --- a/crates/cdk/examples/mint-token.rs +++ b/crates/cdk/examples/mint-token.rs @@ -24,7 +24,7 @@ async fn main() -> Result<(), Error> { let amount = Amount::from(10); // Create a new wallet - let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?; + let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None)?; // Request a mint quote from the wallet let quote = wallet.mint_quote(amount, None).await?; diff --git a/crates/cdk/examples/p2pk.rs b/crates/cdk/examples/p2pk.rs index 85112d061..2e10a8e0c 100644 --- a/crates/cdk/examples/p2pk.rs +++ b/crates/cdk/examples/p2pk.rs @@ -23,7 +23,7 @@ async fn main() -> Result<(), Error> { let amount = Amount::from(10); // Create a new wallet - let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?; + let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None).unwrap(); // Request a mint quote from the wallet let quote = wallet.mint_quote(amount, None).await?; diff --git a/crates/cdk/examples/proof-selection.rs b/crates/cdk/examples/proof-selection.rs index dfd59616d..4ce060d16 100644 --- a/crates/cdk/examples/proof-selection.rs +++ b/crates/cdk/examples/proof-selection.rs @@ -23,7 +23,7 @@ async fn main() -> Result<(), Box> { let localstore = WalletMemoryDatabase::default(); // Create a new wallet - let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?; + let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None).unwrap(); // Amount to mint for amount in [64] { diff --git a/crates/cdk/examples/wallet.rs b/crates/cdk/examples/wallet.rs index dfaca4015..a0e727efe 100644 --- a/crates/cdk/examples/wallet.rs +++ b/crates/cdk/examples/wallet.rs @@ -25,7 +25,7 @@ async fn main() -> Result<(), Box> { let localstore = WalletMemoryDatabase::default(); // Create a new wallet - let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?; + let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None).unwrap(); // Request a mint quote from the wallet let quote = wallet.mint_quote(amount, None).await?; diff --git a/crates/cdk/src/cdk_database/mint_memory.rs b/crates/cdk/src/cdk_database/mint_memory.rs index 86ec3577e..399d36ed7 100644 --- a/crates/cdk/src/cdk_database/mint_memory.rs +++ b/crates/cdk/src/cdk_database/mint_memory.rs @@ -4,9 +4,10 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; -use cdk_common::database::{Error, MintDatabase}; +use cdk_common::database::{Error, MintAuthDatabase, MintDatabase}; use cdk_common::mint::MintKeySetInfo; use cdk_common::nut00::ProofsMethods; +use cdk_common::AuthProof; use tokio::sync::{Mutex, RwLock}; use uuid::Uuid; @@ -413,3 +414,141 @@ impl MintDatabase for MintMemoryDatabase { Ok(ys.get(quote_id).cloned().unwrap_or_default()) } } + +/// Mint Memory Auth Database +#[derive(Debug, Clone, Default)] +#[allow(clippy::type_complexity)] +pub struct MintMemoryAuthDatabase { + active_keyset: Arc>>, + keysets: Arc>>, + proofs: Arc>>, + proof_state: Arc>>, + blinded_signatures: Arc>>, +} + +impl MintMemoryAuthDatabase { + /// Create new [`MintMemoryDatabase`] + #[allow(clippy::too_many_arguments)] + pub fn new( + active_keyset: Option, + keysets: Vec, + spent_proofs: Vec, + blinded_signatures: HashMap<[u8; 33], BlindSignature>, + ) -> Result { + let mut proofs = HashMap::new(); + let mut proof_states = HashMap::new(); + + for proof in spent_proofs { + let y = hash_to_curve(&proof.secret.to_bytes())?.to_bytes(); + proofs.insert(y, proof); + proof_states.insert(y, State::Spent); + } + + Ok(Self { + active_keyset: Arc::new(RwLock::new(active_keyset)), + keysets: Arc::new(RwLock::new( + keysets.into_iter().map(|k| (k.id, k)).collect(), + )), + proofs: Arc::new(RwLock::new(proofs)), + proof_state: Arc::new(Mutex::new(proof_states)), + blinded_signatures: Arc::new(RwLock::new(blinded_signatures)), + }) + } +} + +#[async_trait] +impl MintAuthDatabase for MintMemoryAuthDatabase { + type Err = Error; + + async fn set_active_keyset(&self, id: Id) -> Result<(), Self::Err> { + let mut active = self.active_keyset.write().await; + + *active = Some(id); + + Ok(()) + } + + async fn get_active_keyset_id(&self) -> Result, Self::Err> { + Ok(*self.active_keyset.read().await) + } + + async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> { + self.keysets.write().await.insert(keyset.id, keyset); + Ok(()) + } + + async fn get_keyset_info(&self, keyset_id: &Id) -> Result, Self::Err> { + Ok(self.keysets.read().await.get(keyset_id).cloned()) + } + + async fn get_keyset_infos(&self) -> Result, Self::Err> { + Ok(self.keysets.read().await.values().cloned().collect()) + } + + async fn add_proof(&self, proof: AuthProof) -> Result<(), Self::Err> { + let mut db_proofs = self.proofs.write().await; + + let y = hash_to_curve(&proof.secret.to_bytes())?; + + let y = y.to_bytes(); + + db_proofs.insert(y, proof); + + Ok(()) + } + async fn update_proof_state( + &self, + y: &PublicKey, + proof_state: State, + ) -> Result, Self::Err> { + let mut proofs_states = self.proof_state.lock().await; + + let state = proofs_states.insert(y.to_bytes(), proof_state); + + Ok(state) + } + + async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err> { + let proofs_states = self.proof_state.lock().await; + + let mut states = Vec::new(); + + for y in ys { + let state = proofs_states.get(&y.to_bytes()).cloned(); + states.push(state); + } + + Ok(states) + } + + async fn add_blind_signatures( + &self, + blinded_message: &[PublicKey], + blind_signatures: &[BlindSignature], + ) -> Result<(), Self::Err> { + let mut current_blinded_signatures = self.blinded_signatures.write().await; + + for (blinded_message, blind_signature) in blinded_message.iter().zip(blind_signatures) { + current_blinded_signatures.insert(blinded_message.to_bytes(), blind_signature.clone()); + } + + Ok(()) + } + + async fn get_blind_signatures( + &self, + blinded_messages: &[PublicKey], + ) -> Result>, Self::Err> { + let mut signatures = Vec::with_capacity(blinded_messages.len()); + + let blinded_signatures = self.blinded_signatures.read().await; + + for blinded_message in blinded_messages { + let signature = blinded_signatures.get(&blinded_message.to_bytes()).cloned(); + + signatures.push(signature) + } + + Ok(signatures) + } +} diff --git a/crates/cdk/src/cdk_database/mod.rs b/crates/cdk/src/cdk_database/mod.rs index e2ec6458a..a5244f4fe 100644 --- a/crates/cdk/src/cdk_database/mod.rs +++ b/crates/cdk/src/cdk_database/mod.rs @@ -6,6 +6,6 @@ pub mod mint_memory; pub mod wallet_memory; /// re-export types -pub use cdk_common::database::{Error, MintDatabase, WalletDatabase}; +pub use cdk_common::database::{Error, MintAuthDatabase, MintDatabase, WalletDatabase}; #[cfg(feature = "wallet")] pub use wallet_memory::WalletMemoryDatabase; diff --git a/crates/cdk/src/lib.rs b/crates/cdk/src/lib.rs index 9fffadb3a..dfb6c651d 100644 --- a/crates/cdk/src/lib.rs +++ b/crates/cdk/src/lib.rs @@ -11,6 +11,12 @@ pub mod mint; #[cfg(feature = "wallet")] pub mod wallet; +#[cfg(any(feature = "wallet", feature = "mint"))] +mod oidc_client; + +#[cfg(any(feature = "wallet", feature = "mint"))] +pub use oidc_client::OidcClient; + pub mod pub_sub; /// Re-export amount type @@ -37,7 +43,7 @@ pub use wallet::{Wallet, WalletSubscription}; pub use self::util::SECP256K1; #[cfg(feature = "wallet")] #[doc(hidden)] -pub use self::wallet::client::HttpClient; +pub use self::wallet::HttpClient; /// Result #[doc(hidden)] diff --git a/crates/cdk/src/mint/auth/mod.rs b/crates/cdk/src/mint/auth/mod.rs new file mode 100644 index 000000000..5f5726f2b --- /dev/null +++ b/crates/cdk/src/mint/auth/mod.rs @@ -0,0 +1,182 @@ +use tracing::instrument; + +use super::nutxx::ProtectedEndpoint; +use super::{ + AuthProof, AuthRequired, AuthToken, BlindAuthToken, BlindSignature, BlindedMessage, Error, Id, + Mint, State, +}; +use crate::dhke::{sign_message, verify_message}; +use crate::Amount; + +impl Mint { + /// Check if and what kind of auth is required for a method + pub fn protected(&self, method: &ProtectedEndpoint) -> Option { + self.config.load().protected_endpoints.get(method).copied() + } + + /// Verify Clear auth + pub async fn verify_clear_auth(&self, token: String) -> Result<(), Error> { + Ok(self + .oidc_client + .as_ref() + .ok_or(Error::OidcNotSet)? + .verify_cat(&token) + .await?) + } + + /// Ensure Keyset is loaded in mint + #[instrument(skip(self))] + pub async fn ensure_blind_auth_keyset_loaded(&self, id: &Id) -> Result<(), Error> { + if self.config.load().keysets.contains_key(id) { + return Ok(()); + } + + let mut keysets = self.config.load().keysets.clone(); + let keyset_info = self + .auth_localstore + .as_ref() + .ok_or(Error::AmountKey)? + .get_keyset_info(id) + .await? + .ok_or(Error::KeysetUnknown(*id))?; + + let id = keyset_info.id; + keysets.insert(id, self.generate_keyset(keyset_info)); + self.config.set_keysets(keysets); + Ok(()) + } + + /// Verify Blind auth + pub async fn verify_blind_auth(&self, token: &BlindAuthToken) -> Result<(), Error> { + let proof = &token.auth_proof; + let keyset_id = proof.keyset_id; + + self.ensure_blind_auth_keyset_loaded(&keyset_id).await?; + + let keyset = self + .config + .load() + .keysets + .get(&keyset_id) + .ok_or(Error::UnknownKeySet)? + .clone(); + + let keypair = match keyset.keys.get(&Amount::from(1)) { + Some(key_pair) => key_pair, + None => return Err(Error::AmountKey), + }; + + verify_message(&keypair.secret_key, proof.c, proof.secret.as_bytes())?; + + Ok(()) + } + + /// Verify Auth + /// + /// If it is a blind auth this will also burn the proof + pub async fn verify_auth( + &self, + auth_token: Option, + endpoint: &ProtectedEndpoint, + ) -> Result<(), Error> { + if let Some(auth_required) = self.protected(endpoint) { + let auth_token = auth_token.ok_or(Error::AuthRequired)?; + + match (auth_required, auth_token) { + (AuthRequired::Clear, AuthToken::ClearAuth(token)) => { + self.verify_clear_auth(token).await? + } + (AuthRequired::Blind, AuthToken::BlindAuth(token)) => { + self.verify_blind_auth(&token).await?; + + let auth_proof = token.auth_proof; + + self.check_blind_auth_proof_spendable(auth_proof).await?; + } + (_, _) => return Err(Error::AuthRequired), + } + } + + Ok(()) + } + + /// Check state of blind auth proof and mark it as spent + #[instrument(skip_all)] + pub async fn check_blind_auth_proof_spendable(&self, proof: AuthProof) -> Result<(), Error> { + let auth_localstore = self.auth_localstore.as_ref().ok_or(Error::AmountKey)?; + + auth_localstore.add_proof(proof.clone()).await?; + + let state = auth_localstore + .update_proof_state(&proof.y()?, State::Spent) + .await?; + + match state { + Some(State::Spent) => { + return Err(Error::TokenAlreadySpent); + } + Some(State::Pending) => { + return Err(Error::TokenPending); + } + _ => (), + }; + + Ok(()) + } + + /// Blind Sign + #[instrument(skip_all)] + pub async fn auth_blind_sign( + &self, + blinded_message: &BlindedMessage, + ) -> Result { + let BlindedMessage { + amount, + blinded_secret, + keyset_id, + .. + } = blinded_message; + self.ensure_blind_auth_keyset_loaded(keyset_id).await?; + + let auth_localstore = self + .auth_localstore + .as_ref() + .ok_or(Error::AuthSettingsUndefinded)?; + + let keyset_info = auth_localstore + .get_keyset_info(keyset_id) + .await? + .ok_or(Error::UnknownKeySet)?; + + let active = auth_localstore + .get_active_keyset_id() + .await? + .ok_or(Error::InactiveKeyset)?; + + // Check that the keyset is active and should be used to sign + if keyset_info.id.ne(&active) { + return Err(Error::InactiveKeyset); + } + + let config = self.config.load(); + let keysets = &config.keysets; + let keyset = keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?; + + let key_pair = match keyset.keys.get(amount) { + Some(key_pair) => key_pair, + None => return Err(Error::AmountKey), + }; + + let c = sign_message(&key_pair.secret_key, blinded_secret)?; + + let blinded_signature = BlindSignature::new( + *amount, + c, + keyset_info.id, + &blinded_message.blinded_secret, + key_pair.secret_key.clone(), + )?; + + Ok(blinded_signature) + } +} diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index d40e2b122..3c4afb2a5 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -8,13 +8,14 @@ use cdk_common::database::{self, MintDatabase}; use super::nut17::SupportedMethods; use super::nut19::{self, CachedEndpoint}; -use super::Nuts; +use super::{nutxx, nutxx1, AuthRequired, MintAuthDatabase, Nuts}; use crate::amount::Amount; +use crate::cdk_database; use crate::cdk_lightning::{self, MintLightning}; use crate::mint::Mint; use crate::nuts::{ ContactInfo, CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings, MintVersion, - MppMethodSettings, PaymentMethod, + MppMethodSettings, PaymentMethod, ProtectedEndpoint, }; use crate::types::{LnKey, QuoteTTL}; @@ -27,11 +28,14 @@ pub struct MintBuilder { mint_info: MintInfo, /// Mint Storage backend localstore: Option + Send + Sync>>, + /// Mint Storage backend + auth_localstore: Option + Send + Sync>>, /// Ln backends for mint ln: Option + Send + Sync>>>, seed: Option>, quote_ttl: Option, supported_units: HashMap, + protected_endpoints: HashMap, } impl MintBuilder { @@ -63,6 +67,15 @@ impl MintBuilder { self } + /// Set auth localstore + pub fn with_auth_localstore( + mut self, + localstore: Arc + Send + Sync>, + ) -> MintBuilder { + self.auth_localstore = Some(localstore); + self + } + /// Set mint url pub fn with_mint_url(mut self, mint_url: String) -> Self { self.mint_url = Some(mint_url); @@ -224,6 +237,54 @@ impl MintBuilder { self } + /// Set clear auth settings + pub fn set_cleat_auth_settings( + mut self, + openid_discovery: String, + client_id: String, + protected_endpoints: Vec, + ) -> Self { + let mut nuts = self.mint_info.nuts; + + nuts.nutxx = Some(nutxx::Settings::new( + openid_discovery, + client_id, + protected_endpoints.clone(), + )); + + self.mint_info.nuts = nuts; + + for endpoint in protected_endpoints { + self.protected_endpoints + .insert(endpoint, AuthRequired::Clear); + } + + self + } + + /// Set blind auth settings + pub fn set_blind_auth_settings( + mut self, + bat_max_mint: u64, + protected_endpoints: Vec, + ) -> Self { + let mut nuts = self.mint_info.nuts; + + nuts.nutxx1 = Some(nutxx1::Settings::new( + bat_max_mint, + protected_endpoints.clone(), + )); + + for endpoint in protected_endpoints { + self.protected_endpoints + .insert(endpoint, AuthRequired::Blind); + } + + self.mint_info.nuts = nuts; + + self + } + /// Build mint pub async fn build(&self) -> anyhow::Result { Ok(Mint::new( @@ -234,9 +295,11 @@ impl MintBuilder { self.localstore .clone() .ok_or(anyhow!("Localstore not set"))?, + self.auth_localstore.clone(), self.ln.clone().ok_or(anyhow!("Ln backends not set"))?, self.supported_units.clone(), HashMap::new(), + self.protected_endpoints.clone(), ) .await?) } diff --git a/crates/cdk/src/mint/check_spendable.rs b/crates/cdk/src/mint/check_spendable.rs index 04c009d93..b0a821679 100644 --- a/crates/cdk/src/mint/check_spendable.rs +++ b/crates/cdk/src/mint/check_spendable.rs @@ -2,7 +2,11 @@ use std::collections::HashSet; use tracing::instrument; -use super::{CheckStateRequest, CheckStateResponse, Mint, ProofState, PublicKey, State}; +use super::{ + AuthToken, CheckStateRequest, CheckStateResponse, Method, Mint, ProofState, PublicKey, + RoutePath, State, +}; +use crate::nuts::ProtectedEndpoint; use crate::Error; impl Mint { @@ -10,8 +14,15 @@ impl Mint { #[instrument(skip_all)] pub async fn check_state( &self, + auth_token: Option, check_state: &CheckStateRequest, ) -> Result { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11), + ) + .await?; + let states = self.localstore.get_proofs_states(&check_state.ys).await?; let states = states diff --git a/crates/cdk/src/mint/config.rs b/crates/cdk/src/mint/config.rs index a669cea09..1f48bbcd7 100644 --- a/crates/cdk/src/mint/config.rs +++ b/crates/cdk/src/mint/config.rs @@ -6,8 +6,9 @@ use std::sync::Arc; use arc_swap::ArcSwap; -use super::{Id, MintInfo, MintKeySet}; +use super::{AuthRequired, Id, MintInfo, MintKeySet}; use crate::mint_url::MintUrl; +use crate::nuts::ProtectedEndpoint; use crate::types::QuoteTTL; /// Mint Inner configuration @@ -20,6 +21,8 @@ pub struct Config { pub mint_url: MintUrl, /// Quotes ttl pub quote_ttl: QuoteTTL, + /// Protected methods + pub protected_endpoints: HashMap, } /// Mint configuration @@ -41,12 +44,14 @@ impl SwappableConfig { quote_ttl: QuoteTTL, mint_info: MintInfo, keysets: HashMap, + protected_endpoints: HashMap, ) -> Self { let inner = Config { keysets, quote_ttl, mint_info, mint_url, + protected_endpoints, }; Self { @@ -72,6 +77,7 @@ impl SwappableConfig { quote_ttl: current_inner.quote_ttl, mint_info: current_inner.mint_info.clone(), keysets: current_inner.keysets.clone(), + protected_endpoints: current_inner.protected_endpoints.clone(), }; self.config.store(Arc::new(new_inner)); @@ -90,6 +96,7 @@ impl SwappableConfig { mint_url: current_inner.mint_url.clone(), quote_ttl, keysets: current_inner.keysets.clone(), + protected_endpoints: current_inner.protected_endpoints.clone(), }; self.config.store(Arc::new(new_inner)); @@ -108,6 +115,7 @@ impl SwappableConfig { mint_url: current_inner.mint_url.clone(), quote_ttl: current_inner.quote_ttl, keysets: current_inner.keysets.clone(), + protected_endpoints: current_inner.protected_endpoints.clone(), }; self.config.store(Arc::new(new_inner)); @@ -121,6 +129,7 @@ impl SwappableConfig { quote_ttl: current_inner.quote_ttl, mint_url: current_inner.mint_url.clone(), keysets, + protected_endpoints: current_inner.protected_endpoints.clone(), }; self.config.store(Arc::new(new_inner)); diff --git a/crates/cdk/src/mint/issue/auth.rs b/crates/cdk/src/mint/issue/auth.rs new file mode 100644 index 000000000..ea2fb373d --- /dev/null +++ b/crates/cdk/src/mint/issue/auth.rs @@ -0,0 +1,53 @@ +use tracing::instrument; + +use crate::mint::nutxx1::MintAuthRequest; +use crate::mint::{AuthToken, MintBolt11Response}; +use crate::{Amount, Error, Mint}; + +impl Mint { + /// Mint Auth Proofs + #[instrument(skip_all)] + pub async fn mint_blind_auth( + &self, + auth_token: AuthToken, + mint_auth_request: MintAuthRequest, + ) -> Result { + let cat = if let AuthToken::ClearAuth(cat) = auth_token { + cat + } else { + tracing::debug!("Received blind auth mint without cat"); + return Err(Error::AuthRequired); + }; + + self.verify_clear_auth(cat).await?; + + let auth_settings = self + .mint_info() + .nuts + .nutxx1 + .ok_or(Error::AuthSettingsUndefinded)?; + + if mint_auth_request.amount() > auth_settings.bat_max_mint { + return Err(Error::AmountOutofLimitRange( + 1.into(), + auth_settings.bat_max_mint.into(), + mint_auth_request.amount().into(), + )); + } + + let mut blind_signatures = Vec::with_capacity(mint_auth_request.outputs.len()); + + for blinded_message in mint_auth_request.outputs.iter() { + if blinded_message.amount != Amount::from(1) { + return Err(Error::AmountKey); + } + + let blind_signature = self.auth_blind_sign(blinded_message).await?; + blind_signatures.push(blind_signature); + } + + Ok(MintBolt11Response { + signatures: blind_signatures, + }) + } +} diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/issue/issue_nut04.rs similarity index 91% rename from crates/cdk/src/mint/mint_nut04.rs rename to crates/cdk/src/mint/issue/issue_nut04.rs index f253486b7..4871c4a05 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/issue/issue_nut04.rs @@ -1,14 +1,14 @@ use tracing::instrument; use uuid::Uuid; -use super::{ - nut04, CurrencyUnit, Mint, MintQuote, MintQuoteBolt11Request, MintQuoteBolt11Response, - NotificationPayload, PaymentMethod, PublicKey, +use crate::mint::{ + CurrencyUnit, MintBolt11Request, MintBolt11Response, MintQuote, MintQuoteBolt11Request, + MintQuoteBolt11Response, MintQuoteState, NotificationPayload, PublicKey, }; -use crate::nuts::MintQuoteState; +use crate::nuts::{AuthToken, Method, PaymentMethod, ProtectedEndpoint, RoutePath}; use crate::types::LnKey; use crate::util::unix_time; -use crate::{Amount, Error}; +use crate::{Amount, Error, Mint}; impl Mint { /// Checks that minting is enabled, request is supported unit and within range @@ -60,8 +60,15 @@ impl Mint { #[instrument(skip_all)] pub async fn get_mint_bolt11_quote( &self, + auth_token: Option, mint_quote_request: MintQuoteBolt11Request, ) -> Result, Error> { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11), + ) + .await?; + let MintQuoteBolt11Request { amount, unit, @@ -132,8 +139,15 @@ impl Mint { #[instrument(skip(self))] pub async fn check_mint_quote( &self, + auth_token: Option, quote_id: &Uuid, ) -> Result, Error> { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11), + ) + .await?; + let quote = self .localstore .get_mint_quote(quote_id) @@ -258,8 +272,15 @@ impl Mint { #[instrument(skip_all)] pub async fn process_mint_request( &self, - mint_request: nut04::MintBolt11Request, - ) -> Result { + auth_token: Option, + mint_request: MintBolt11Request, + ) -> Result { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11), + ) + .await?; + let mint_quote = if let Some(mint_quote) = self.localstore.get_mint_quote(&mint_request.quote).await? { mint_quote @@ -346,7 +367,7 @@ impl Mint { self.pubsub_manager .mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued); - Ok(nut04::MintBolt11Response { + Ok(MintBolt11Response { signatures: blind_signatures, }) } diff --git a/crates/cdk/src/mint/issue/mod.rs b/crates/cdk/src/mint/issue/mod.rs new file mode 100644 index 000000000..63b46361e --- /dev/null +++ b/crates/cdk/src/mint/issue/mod.rs @@ -0,0 +1,2 @@ +mod auth; +mod issue_nut04; diff --git a/crates/cdk/src/mint/keysets/auth.rs b/crates/cdk/src/mint/keysets/auth.rs new file mode 100644 index 000000000..7065a20f4 --- /dev/null +++ b/crates/cdk/src/mint/keysets/auth.rs @@ -0,0 +1,66 @@ +//! Auth keyset functions +use std::collections::HashSet; + +use tracing::instrument; + +use crate::mint::{CurrencyUnit, Id, KeySetInfo, KeysResponse, KeysetResponse}; +use crate::{Error, Mint}; + +impl Mint { + /// Retrieve the auth public keys of the active keyset for distribution to wallet + /// clients + #[instrument(skip_all)] + pub async fn auth_pubkeys(&self) -> Result { + let active_keyset_id = self + .auth_localstore + .as_ref() + .ok_or(Error::AuthLocalstoreUndefinded)? + .get_active_keyset_id() + .await? + .ok_or(Error::AmountKey)?; + + self.ensure_blind_auth_keyset_loaded(&active_keyset_id) + .await?; + + let keysets = self.config.load().keysets.clone(); + + Ok(KeysResponse { + keysets: vec![keysets + .get(&active_keyset_id) + .ok_or(Error::KeysetUnknown(active_keyset_id))? + .clone() + .into()], + }) + } + + /// Return a list of auth keysets + #[instrument(skip_all)] + pub async fn auth_keysets(&self) -> Result { + let keysets = self + .auth_localstore + .clone() + .unwrap() + .get_keyset_infos() + .await?; + let active_keysets: HashSet = self + .localstore + .get_active_keysets() + .await? + .values() + .cloned() + .collect(); + + let keysets = keysets + .into_iter() + .filter(|k| k.unit == CurrencyUnit::Auth) + .map(|k| KeySetInfo { + id: k.id, + unit: k.unit, + active: active_keysets.contains(&k.id), + input_fee_ppk: k.input_fee_ppk, + }) + .collect(); + + Ok(KeysetResponse { keysets }) + } +} diff --git a/crates/cdk/src/mint/keysets.rs b/crates/cdk/src/mint/keysets/mod.rs similarity index 94% rename from crates/cdk/src/mint/keysets.rs rename to crates/cdk/src/mint/keysets/mod.rs index abad59839..8fa96e58f 100644 --- a/crates/cdk/src/mint/keysets.rs +++ b/crates/cdk/src/mint/keysets/mod.rs @@ -9,6 +9,8 @@ use super::{ }; use crate::Error; +mod auth; + impl Mint { /// Retrieve the public keys of the active keyset for distribution to wallet /// clients @@ -31,7 +33,10 @@ impl Mint { /// clients #[instrument(skip_all)] pub async fn pubkeys(&self) -> Result { - let active_keysets = self.localstore.get_active_keysets().await?; + let mut active_keysets = self.localstore.get_active_keysets().await?; + + // We don't want to return auth keys here even though in the db we treat them the same + active_keysets.remove(&CurrencyUnit::Auth); let active_keysets: HashSet<&Id> = active_keysets.values().collect(); @@ -67,6 +72,7 @@ impl Mint { let keysets = keysets .into_iter() + .filter(|k| k.unit != CurrencyUnit::Auth) .map(|k| KeySetInfo { id: k.id, unit: k.unit, diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index f2679b522..802dcb894 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -8,13 +8,14 @@ use tracing::instrument; use uuid::Uuid; use super::{ - CurrencyUnit, MeltBolt11Request, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, - Mint, PaymentMethod, PublicKey, State, + AuthToken, CurrencyUnit, MeltBolt11Request, MeltQuote, MeltQuoteBolt11Request, + MeltQuoteBolt11Response, Mint, PaymentMethod, PublicKey, State, }; use crate::amount::to_unit; use crate::cdk_lightning::{MintLightning, PayInvoiceResponse}; use crate::mint::SigFlag; use crate::nuts::nut11::{enforce_sig_flag, EnforceSigFlag}; +use crate::nuts::nutxx::{Method, ProtectedEndpoint, RoutePath}; use crate::nuts::{Id, MeltQuoteState}; use crate::types::LnKey; use crate::util::unix_time; @@ -54,8 +55,15 @@ impl Mint { #[instrument(skip_all)] pub async fn get_melt_bolt11_quote( &self, + auth_token: Option, melt_request: &MeltQuoteBolt11Request, ) -> Result, Error> { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11), + ) + .await?; + let MeltQuoteBolt11Request { request, unit, @@ -119,8 +127,15 @@ impl Mint { #[instrument(skip(self))] pub async fn check_melt_quote( &self, + auth_token: Option, quote_id: &Uuid, ) -> Result, Error> { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Get, RoutePath::MeltBolt11), + ) + .await?; + let quote = self .localstore .get_melt_quote(quote_id) @@ -379,8 +394,15 @@ impl Mint { #[instrument(skip_all)] pub async fn melt_bolt11( &self, + auth_token: Option, melt_request: &MeltBolt11Request, ) -> Result, Error> { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11), + ) + .await?; + use std::sync::Arc; async fn check_payment_state( ln: Arc + Send + Sync>, diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 23fd96e12..b74e65663 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/crates/cdk/src/mint/mod.rs @@ -7,10 +7,11 @@ use std::sync::Arc; use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv}; use bitcoin::secp256k1::{self, Secp256k1}; use cdk_common::common::{LnKey, QuoteTTL}; -use cdk_common::database::{self, MintDatabase}; +use cdk_common::database::{self, MintAuthDatabase, MintDatabase}; use cdk_common::mint::MintKeySetInfo; use config::SwappableConfig; use futures::StreamExt; +use nutxx::ProtectedEndpoint; use serde::{Deserialize, Serialize}; use subscription::PubSubManager; use tokio::sync::Notify; @@ -25,15 +26,16 @@ use crate::fees::calculate_fee; use crate::mint_url::MintUrl; use crate::nuts::*; use crate::util::unix_time; -use crate::Amount; +use crate::{Amount, OidcClient}; +pub(crate) mod auth; mod builder; mod check_spendable; pub mod config; mod info; +mod issue; mod keysets; mod melt; -mod mint_nut04; mod start_up_check; pub mod subscription; mod swap; @@ -48,10 +50,13 @@ pub struct Mint { pub config: SwappableConfig, /// Mint Storage backend pub localstore: Arc + Send + Sync>, + /// Mint Storage backend + pub auth_localstore: Option + Send + Sync>>, /// Ln backends for mint pub ln: HashMap + Send + Sync>>, /// Subscription manager pub pubsub_manager: Arc, + oidc_client: Option, secp_ctx: Secp256k1, xpriv: Xpriv, } @@ -65,10 +70,12 @@ impl Mint { mint_info: MintInfo, quote_ttl: QuoteTTL, localstore: Arc + Send + Sync>, + auth_localstore: Option + Send + Sync>>, ln: HashMap + Send + Sync>>, // Hashmap where the key is the unit and value is (input fee ppk, max_order) supported_units: HashMap, custom_paths: HashMap, + protected_endpoints: HashMap, ) -> Result { let secp_ctx = Secp256k1::new(); let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted"); @@ -182,18 +189,55 @@ impl Mint { } } + let oidc_client = if let Some(nutxx_settings) = &mint_info.nuts.nutxx { + tracing::info!("Auth enabled creating auth keysets"); + let auth_localstore = auth_localstore + .as_ref() + .ok_or(Error::AuthSettingsUndefinded)?; + + let derivation_path = match custom_paths.get(&CurrencyUnit::Auth) { + Some(path) => path.clone(), + None => derivation_path_from_unit(CurrencyUnit::Auth, 0) + .ok_or(Error::UnsupportedUnit)?, + }; + + let (keyset, keyset_info) = create_new_keyset( + &secp_ctx, + xpriv, + derivation_path, + Some(0), + CurrencyUnit::Auth, + 1, + 0, + ); + + println!("{:?}", keyset_info); + + let id = keyset_info.id; + auth_localstore.add_keyset_info(keyset_info).await?; + auth_localstore.set_active_keyset(id).await?; + active_keysets.insert(id, keyset); + + Some(OidcClient::new(nutxx_settings.openid_discovery.clone())) + } else { + None + }; + Ok(Self { config: SwappableConfig::new( MintUrl::from_str(mint_url)?, quote_ttl, mint_info, active_keysets, + protected_endpoints, ), pubsub_manager: Arc::new(localstore.clone().into()), secp_ctx, xpriv, localstore, + oidc_client, ln, + auth_localstore, }) } @@ -431,7 +475,17 @@ impl Mint { /// Restore #[instrument(skip_all)] - pub async fn restore(&self, request: RestoreRequest) -> Result { + pub async fn restore( + &self, + auth_token: Option, + request: RestoreRequest, + ) -> Result { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11), + ) + .await?; + let output_len = request.outputs.len(); let mut outputs = Vec::with_capacity(output_len); @@ -702,9 +756,11 @@ mod tests { config.mint_info, config.quote_ttl, localstore, + None, HashMap::new(), config.supported_units, HashMap::new(), + HashMap::new(), ) .await } diff --git a/crates/cdk/src/mint/swap.rs b/crates/cdk/src/mint/swap.rs index 6dcf31fa0..671d6c8a6 100644 --- a/crates/cdk/src/mint/swap.rs +++ b/crates/cdk/src/mint/swap.rs @@ -3,7 +3,8 @@ use std::collections::HashSet; use tracing::instrument; use super::nut11::{enforce_sig_flag, EnforceSigFlag}; -use super::{Id, Mint, PublicKey, SigFlag, State, SwapRequest, SwapResponse}; +use super::nutxx::{Method, ProtectedEndpoint, RoutePath}; +use super::{AuthToken, Id, Mint, PublicKey, SigFlag, State, SwapRequest, SwapResponse}; use crate::nuts::nut00::ProofsMethods; use crate::Error; @@ -12,8 +13,15 @@ impl Mint { #[instrument(skip_all)] pub async fn process_swap_request( &self, + auth_token: Option, swap_request: SwapRequest, ) -> Result { + self.verify_auth( + auth_token, + &ProtectedEndpoint::new(Method::Post, RoutePath::Swap), + ) + .await?; + let blinded_messages: Vec = swap_request .outputs .iter() diff --git a/crates/cdk/src/oidc_client.rs b/crates/cdk/src/oidc_client.rs new file mode 100644 index 000000000..55e4edbde --- /dev/null +++ b/crates/cdk/src/oidc_client.rs @@ -0,0 +1,219 @@ +//! Open Id Connect + +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::Arc; + +use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet}; +use jsonwebtoken::{decode, decode_header, DecodingKey, Validation}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wallet")] +use serde_json::Value; +use thiserror::Error; +use tokio::sync::RwLock; +use tracing::instrument; + +/// OIDC Error +#[derive(Debug, Error)] +pub enum Error { + /// From Reqwest error + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + /// From Reqwest error + #[error(transparent)] + Jwt(#[from] jsonwebtoken::errors::Error), + /// Missing kid header + #[error("Missing kid header")] + MissingKidHeader, + /// Missing jwk header + #[error("Missing jwk")] + MissingJwkHeader, + /// Unsupported Algo + #[error("Unsupported signing algo")] + UnsupportedSigningAlgo, + /// Access token not returned + #[error("Error getting access token")] + AccessTokenMissing, +} + +impl From for cdk_common::error::Error { + fn from(value: Error) -> Self { + cdk_common::error::Error::Custom(value.to_string()) + } +} + +/// Open Id Config +#[derive(Debug, Clone, Deserialize)] +pub struct OidcConfig { + pub jwks_uri: String, + pub issuer: String, + pub token_endpoint: String, +} + +/// Http Client +#[derive(Debug, Clone)] +pub struct OidcClient { + client: Client, + openid_discovery: String, + oidc_config: Arc>>, + jwks_set: Arc>>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct AccessTokenRequest { + pub grant_type: String, + pub client_id: String, + pub username: String, + pub password: String, +} + +impl OidcClient { + /// Create new [`OidcClient`] + pub fn new(openid_discovery: String) -> Self { + Self { + client: Client::new(), + openid_discovery, + oidc_config: Arc::new(RwLock::new(None)), + jwks_set: Arc::new(RwLock::new(None)), + } + } + + /// Get config from oidc server + #[instrument(skip(self))] + pub async fn get_oidc_config(&self) -> Result { + tracing::debug!("Getting oidc config"); + let oidc_config = self + .client + .get(&self.openid_discovery) + .send() + .await? + .json::() + .await?; + + let mut current_config = self.oidc_config.write().await; + + *current_config = Some(oidc_config.clone()); + + Ok(oidc_config) + } + + /// Get jwk set + #[instrument(skip(self))] + pub async fn get_jwkset(&self, jwks_uri: &str) -> Result { + tracing::debug!("Getting jwks set"); + let jwks_set = self + .client + .get(jwks_uri) + .send() + .await? + .json::() + .await?; + + let mut current_set = self.jwks_set.write().await; + + *current_set = Some(jwks_set.clone()); + + Ok(jwks_set) + } + + /// Verify cat token + #[instrument(skip(self, cat_jwt))] + pub async fn verify_cat(&self, cat_jwt: &str) -> Result<(), Error> { + tracing::debug!("Verifying cat"); + let header = decode_header(cat_jwt)?; + + let kid = header.kid.ok_or(Error::MissingKidHeader)?; + + let oidc_config = { + let locked = self.oidc_config.read().await; + match locked.deref() { + Some(config) => config.clone(), + None => { + drop(locked); + self.get_oidc_config().await? + } + } + }; + + let jwks = { + let locked = self.jwks_set.read().await; + match locked.deref() { + Some(set) => set.clone(), + None => { + drop(locked); + self.get_jwkset(&oidc_config.jwks_uri).await? + } + } + }; + + let jwk = match jwks.find(&kid) { + Some(jwk) => jwk.clone(), + None => { + let refreshed_jwks = self.get_jwkset(&oidc_config.jwks_uri).await?; + refreshed_jwks + .find(&kid) + .ok_or(Error::MissingKidHeader)? + .clone() + } + }; + + let decoding_key = match &jwk.algorithm { + AlgorithmParameters::RSA(rsa) => DecodingKey::from_rsa_components(&rsa.n, &rsa.e)?, + AlgorithmParameters::EllipticCurve(ecdsa) => { + DecodingKey::from_ec_components(&ecdsa.x, &ecdsa.y)? + } + _ => return Err(Error::UnsupportedSigningAlgo), + }; + + let validation = { + let mut validation = Validation::new(header.alg); + validation.validate_exp = true; + // REVIEW: Mint doesnt verify aud but i think wallet does? + validation.validate_aud = false; + // validation.set_issuer(&[oidc_config.issuer]); + validation + }; + + if let Err(err) = + decode::>(cat_jwt, &decoding_key, &validation) + { + tracing::debug!("Could not verify cat: {}", err); + return Err(err.into()); + } + + Ok(()) + } + + /// Get Access token (CAT) + #[cfg(feature = "wallet")] + pub async fn get_access_token_with_user_password( + &self, + username: String, + password: String, + ) -> Result { + let token_url = self.get_oidc_config().await?.token_endpoint; + + let request = AccessTokenRequest { + grant_type: "password".to_string(), + client_id: "cashu-client".to_string(), + username, + password, + }; + + let response: Value = self + .client + .post(token_url) + .form(&request) + .send() + .await? + .json() + .await?; + + let token = response + .get("access_token") + .ok_or(Error::AccessTokenMissing)?; + + Ok(token.to_string()) + } +} diff --git a/crates/cdk/src/wallet/README.md b/crates/cdk/src/wallet/README.md index 49d8114c7..84e0e02d6 100644 --- a/crates/cdk/src/wallet/README.md +++ b/crates/cdk/src/wallet/README.md @@ -18,5 +18,5 @@ The CDK [`Wallet`] is a high level Cashu wallet. The [`Wallet`] is for a single let unit = CurrencyUnit::Sat; let localstore = WalletMemoryDatabase::default(); - let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None); + let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None); ``` diff --git a/crates/cdk/src/wallet/auth.rs b/crates/cdk/src/wallet/auth.rs new file mode 100644 index 000000000..2091fc4e0 --- /dev/null +++ b/crates/cdk/src/wallet/auth.rs @@ -0,0 +1,206 @@ +use tracing::instrument; + +use super::Wallet; +use crate::amount::SplitTarget; +use crate::dhke::construct_proofs; +use crate::nuts::nut00::ProofsMethods; +use crate::nuts::nutxx1::MintAuthRequest; +use crate::nuts::{ + AuthRequired, AuthToken, BlindAuthToken, CurrencyUnit, KeySetInfo, PreMintSecrets, Proofs, + ProtectedEndpoint, State, +}; +use crate::types::ProofInfo; +use crate::{Amount, Error}; + +impl Wallet { + /// Get active keyset for mint + /// + /// Queries mint for current keysets then gets [`Keys`] for any unknown + /// keysets + #[instrument(skip(self))] + pub async fn get_active_mint_blind_auth_keysets(&self) -> Result, Error> { + let keysets = self.client.get_mint_blind_auth_keysets().await?; + let keysets = keysets.keysets; + + self.localstore + .add_mint_keysets(self.mint_url.clone(), keysets.clone()) + .await?; + + let active_keysets = keysets + .clone() + .into_iter() + .filter(|k| k.unit == CurrencyUnit::Auth) + .collect::>(); + + match self + .localstore + .get_mint_keysets(self.mint_url.clone()) + .await? + { + Some(known_keysets) => { + let unknown_keysets: Vec<&KeySetInfo> = keysets + .iter() + .filter(|k| known_keysets.contains(k)) + .collect(); + + for keyset in unknown_keysets { + self.get_keyset_keys(keyset.id).await?; + } + } + None => { + for keyset in keysets { + self.get_keyset_keys(keyset.id).await?; + } + } + } + Ok(active_keysets) + } + + /// Get active keyset for mint + /// + /// Queries mint for current keysets then gets [`Keys`] for any unknown + /// keysets + #[instrument(skip(self))] + pub async fn get_active_mint_blind_auth_keyset(&self) -> Result { + let active_keysets = self.get_active_mint_blind_auth_keysets().await?; + + let keyset = active_keysets.first().ok_or(Error::NoActiveKeyset)?; + Ok(keyset.clone()) + } + + /// Get unspent proofs for mint + #[instrument(skip(self))] + pub async fn get_unspent_auth_proofs(&self) -> Result { + Ok(self + .localstore + .get_proofs( + Some(self.mint_url.clone()), + Some(CurrencyUnit::Auth), + Some(vec![State::Unspent]), + None, + ) + .await? + .into_iter() + .map(|p| p.proof) + .collect()) + } + + /// Check if and what kind of auth is required for a method + pub async fn protected(&self, method: &ProtectedEndpoint) -> Option { + let protected_endpoints = self.protected_endpoints.read().await; + protected_endpoints.get(method).copied() + } + + /// Get Auth Token + pub async fn get_blind_auth_token(&self) -> Result, Error> { + let unspent = self.get_unspent_auth_proofs().await?; + + let auth_proof = match unspent.first() { + Some(proof) => { + self.localstore + .update_proofs(vec![], vec![proof.y()?]) + .await?; + proof + } + None => return Ok(None), + }; + + Ok(Some(AuthToken::BlindAuth(BlindAuthToken { + auth_proof: auth_proof.clone().into(), + }))) + } + + /// Auth for request + pub async fn get_auth_for_request( + &self, + method: &ProtectedEndpoint, + ) -> Result, Error> { + let protected_endpoints = self.protected_endpoints.read().await; + + match protected_endpoints.get(method) { + Some(auth) => match auth { + AuthRequired::Clear => Ok(Some(AuthToken::ClearAuth( + self.cat + .clone() + .read() + .await + .as_ref() + .ok_or(Error::AuthRequired)? + .clone(), + ))), + AuthRequired::Blind => { + let proof = self + .get_blind_auth_token() + .await? + .ok_or(Error::InsufficientBlindAuthTokens)?; + + Ok(Some(proof)) + } + }, + None => Ok(None), + } + } + + /// Mint blind auth + #[instrument(skip(self))] + pub async fn mint_blind_auth(&self, amount: Amount) -> Result { + tracing::debug!("Minting {} blind auth proofs", amount); + let cat = self.cat.read().await.clone().ok_or(Error::CatNotSet)?; + // Check that mint is in store of mints + if self + .localstore + .get_mint(self.mint_url.clone()) + .await? + .is_none() + { + self.get_mint_info().await?; + } + + let active_keyset_id = self.get_active_mint_blind_auth_keyset().await?.id; + + let premint_secrets = + PreMintSecrets::random(active_keyset_id, amount, &SplitTarget::Value(1.into()))?; + + let request = MintAuthRequest { + outputs: premint_secrets.blinded_messages(), + }; + + let mint_res = self + .client + .post_mint_blind_auth(request, AuthToken::ClearAuth(cat)) + .await?; + + let keys = self.get_keyset_keys(active_keyset_id).await?; + + let proofs = construct_proofs( + mint_res.signatures, + premint_secrets.rs(), + premint_secrets.secrets(), + &keys, + )?; + + let proof_infos = proofs + .clone() + .into_iter() + .map(|proof| { + ProofInfo::new( + proof, + self.mint_url.clone(), + State::Unspent, + crate::nuts::CurrencyUnit::Auth, + ) + }) + .collect::, _>>()?; + + // Add new proofs to store + self.localstore.update_proofs(proof_infos, vec![]).await?; + + Ok(proofs) + } + + /// Total unspent balance of wallet + #[instrument(skip(self))] + pub async fn total_blind_auth_balance(&self) -> Result { + Ok(self.get_unspent_auth_proofs().await?.total_amount()?) + } +} diff --git a/crates/cdk/src/wallet/keysets.rs b/crates/cdk/src/wallet/keysets.rs index a605a060c..73826786b 100644 --- a/crates/cdk/src/wallet/keysets.rs +++ b/crates/cdk/src/wallet/keysets.rs @@ -1,6 +1,6 @@ use tracing::instrument; -use crate::nuts::{Id, KeySetInfo, Keys}; +use crate::nuts::{Id, KeySetInfo, Keys, Method, ProtectedEndpoint, RoutePath}; use crate::{Error, Wallet}; impl Wallet { @@ -13,7 +13,10 @@ impl Wallet { let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? { keys } else { - let keys = self.client.get_mint_keyset(keyset_id).await?; + let request_auth = self + .get_auth_for_request(&ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11)) + .await?; + let keys = self.client.get_mint_keyset(keyset_id, request_auth).await?; keys.verify_id()?; @@ -30,7 +33,8 @@ impl Wallet { /// Queries mint for all keysets #[instrument(skip(self))] pub async fn get_mint_keysets(&self) -> Result, Error> { - let keysets = self.client.get_mint_keysets().await?; + // REVIEW: Should these be authed + let keysets = self.client.get_mint_keysets(None).await?; self.localstore .add_mint_keysets(self.mint_url.clone(), keysets.keysets.clone()) @@ -45,7 +49,8 @@ impl Wallet { /// keysets #[instrument(skip(self))] pub async fn get_active_mint_keysets(&self) -> Result, Error> { - let keysets = self.client.get_mint_keysets().await?; + // REVIEW: Should these be authed + let keysets = self.client.get_mint_keysets(None).await?; let keysets = keysets.keysets; self.localstore diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index 81aacffd4..0121691bd 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -8,7 +8,7 @@ use crate::amount::to_unit; use crate::dhke::construct_proofs; use crate::nuts::{ CurrencyUnit, MeltBolt11Request, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, - PreMintSecrets, Proofs, ProofsMethods, State, + Method, PreMintSecrets, Proofs, ProofsMethods, ProtectedEndpoint, RoutePath, State, }; use crate::types::{Melted, ProofInfo}; use crate::util::unix_time; @@ -32,7 +32,7 @@ impl Wallet { /// let unit = CurrencyUnit::Sat; /// /// let localstore = WalletMemoryDatabase::default(); - /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None).unwrap(); /// let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string(); /// let quote = wallet.melt_quote(bolt11, None).await?; /// @@ -60,7 +60,17 @@ impl Wallet { options, }; - let quote_res = self.client.post_melt_quote(quote_request).await?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new( + Method::Get, + RoutePath::MeltQuoteBolt11, + )) + .await?; + + let quote_res = self + .client + .post_melt_quote(quote_request, auth_token) + .await?; if quote_res.amount != amount_quote_unit { tracing::warn!( @@ -93,7 +103,17 @@ impl Wallet { &self, quote_id: &str, ) -> Result, Error> { - let response = self.client.get_melt_quote_status(quote_id).await?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new( + Method::Get, + RoutePath::MeltQuoteBolt11, + )) + .await?; + + let response = self + .client + .get_melt_quote_status(quote_id, auth_token) + .await?; match self.localstore.get_melt_quote(quote_id).await? { Some(quote) => { @@ -154,7 +174,11 @@ impl Wallet { outputs: Some(premint_secrets.blinded_messages()), }; - let melt_response = self.client.post_melt(request).await; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11)) + .await?; + + let melt_response = self.client.post_melt(request, auth_token).await; let melt_response = match melt_response { Ok(melt_response) => melt_response, @@ -261,7 +285,7 @@ impl Wallet { /// let unit = CurrencyUnit::Sat; /// /// let localstore = WalletMemoryDatabase::default(); - /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None).unwrap(); /// let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string(); /// let quote = wallet.melt_quote(bolt11, None).await?; /// let quote_id = quote.id; diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index e340cca3e..17b58584f 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -5,8 +5,8 @@ use crate::amount::SplitTarget; use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ - nut12, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, - Proofs, SecretKey, SpendingConditions, State, + nut12, Method, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, + PreMintSecrets, Proofs, ProtectedEndpoint, RoutePath, SecretKey, SpendingConditions, State, }; use crate::types::ProofInfo; use crate::util::unix_time; @@ -32,7 +32,7 @@ impl Wallet { /// let unit = CurrencyUnit::Sat; /// /// let localstore = WalletMemoryDatabase::default(); - /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?; + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None)?; /// let amount = Amount::from(100); /// /// let quote = wallet.mint_quote(amount, None).await?; @@ -74,7 +74,14 @@ impl Wallet { pubkey: Some(secret_key.public_key()), }; - let quote_res = self.client.post_mint_quote(request).await?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new( + Method::Post, + RoutePath::MintQuoteBolt11, + )) + .await?; + + let quote_res = self.client.post_mint_quote(request, auth_token).await?; let quote = MintQuote { mint_url, @@ -98,7 +105,17 @@ impl Wallet { &self, quote_id: &str, ) -> Result, Error> { - let response = self.client.get_mint_quote_status(quote_id).await?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new( + Method::Post, + RoutePath::MintQuoteBolt11, + )) + .await?; + + let response = self + .client + .get_mint_quote_status(quote_id, auth_token) + .await?; match self.localstore.get_mint_quote(quote_id).await? { Some(quote) => { @@ -157,7 +174,7 @@ impl Wallet { /// let unit = CurrencyUnit::Sat; /// /// let localstore = WalletMemoryDatabase::default(); - /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None).unwrap(); /// let amount = Amount::from(100); /// /// let quote = wallet.mint_quote(amount, None).await?; @@ -233,7 +250,11 @@ impl Wallet { request.sign(secret_key)?; } - let mint_res = self.client.post_mint(request).await?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11)) + .await?; + + let mint_res = self.client.post_mint(request, auth_token).await?; let keys = self.get_keyset_keys(active_keyset_id).await?; diff --git a/crates/cdk/src/wallet/client.rs b/crates/cdk/src/wallet/mint_connector/http_client.rs similarity index 63% rename from crates/cdk/src/wallet/client.rs rename to crates/cdk/src/wallet/mint_connector/http_client.rs index 6059a45e9..45906c944 100644 --- a/crates/cdk/src/wallet/client.rs +++ b/crates/cdk/src/wallet/mint_connector/http_client.rs @@ -1,7 +1,3 @@ -//! Wallet client - -use std::fmt::Debug; - use async_trait::async_trait; use reqwest::{Client, IntoUrl}; use serde::de::DeserializeOwned; @@ -10,11 +6,12 @@ use tracing::instrument; #[cfg(not(target_arch = "wasm32"))] use url::Url; -use super::Error; +use super::{Error, MintConnector}; use crate::error::ErrorResponse; use crate::mint_url::MintUrl; +use crate::nuts::nutxx1::MintAuthRequest; use crate::nuts::{ - CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse, + AuthToken, CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, @@ -37,10 +34,18 @@ impl HttpClient { } #[inline] - async fn http_get(&self, url: U) -> Result { - let response = self - .inner - .get(url) + async fn http_get( + &self, + url: U, + auth_token: Option, + ) -> Result { + let mut request = self.inner.get(url); + + if let Some(auth) = auth_token { + request = request.header(auth.header_key(), auth.to_string()); + } + + let response = request .send() .await .map_err(|e| Error::HttpError(e.to_string()))? @@ -61,12 +66,16 @@ impl HttpClient { async fn http_post( &self, url: U, + auth_token: Option, payload: &P, ) -> Result { - let response = self - .inner - .post(url) - .json(&payload) + let mut request = self.inner.post(url).json(&payload); + + if let Some(auth) = auth_token { + request = request.header(auth.header_key(), auth.to_string()); + } + + let response = request .send() .await .map_err(|e| Error::HttpError(e.to_string()))? @@ -124,30 +133,38 @@ impl HttpClient { impl MintConnector for HttpClient { /// Get Active Mint Keys [NUT-01] #[instrument(skip(self), fields(mint_url = %self.mint_url))] - async fn get_mint_keys(&self) -> Result, Error> { + async fn get_mint_keys(&self, auth_token: Option) -> Result, Error> { let url = self.mint_url.join_paths(&["v1", "keys"])?; - Ok(self.http_get::<_, KeysResponse>(url).await?.keysets) + + Ok(self + .http_get::<_, KeysResponse>(url, auth_token) + .await? + .keysets) } /// Get Keyset Keys [NUT-01] #[instrument(skip(self), fields(mint_url = %self.mint_url))] - async fn get_mint_keyset(&self, keyset_id: Id) -> Result { + async fn get_mint_keyset( + &self, + keyset_id: Id, + auth_token: Option, + ) -> Result { let url = self .mint_url .join_paths(&["v1", "keys", &keyset_id.to_string()])?; - self.http_get::<_, KeysResponse>(url) - .await? - .keysets - .drain(0..1) - .next() - .ok_or_else(|| Error::UnknownKeySet) + let keys_response = self.http_get::<_, KeysResponse>(url, auth_token).await?; + + Ok(keys_response.keysets.first().unwrap().clone()) } /// Get Keysets [NUT-02] #[instrument(skip(self), fields(mint_url = %self.mint_url))] - async fn get_mint_keysets(&self) -> Result { + async fn get_mint_keysets( + &self, + auth_token: Option, + ) -> Result { let url = self.mint_url.join_paths(&["v1", "keysets"])?; - self.http_get(url).await + self.http_get(url, auth_token).await } /// Mint Quote [NUT-04] @@ -155,11 +172,12 @@ impl MintConnector for HttpClient { async fn post_mint_quote( &self, request: MintQuoteBolt11Request, + auth_token: Option, ) -> Result, Error> { let url = self .mint_url .join_paths(&["v1", "mint", "quote", "bolt11"])?; - self.http_post(url, &request).await + self.http_post(url, auth_token, &request).await } /// Mint Quote status @@ -167,12 +185,13 @@ impl MintConnector for HttpClient { async fn get_mint_quote_status( &self, quote_id: &str, + auth_token: Option, ) -> Result, Error> { let url = self .mint_url .join_paths(&["v1", "mint", "quote", "bolt11", quote_id])?; - self.http_get(url).await + self.http_get(url, auth_token).await } /// Mint Tokens [NUT-04] @@ -180,9 +199,10 @@ impl MintConnector for HttpClient { async fn post_mint( &self, request: MintBolt11Request, + auth_token: Option, ) -> Result { let url = self.mint_url.join_paths(&["v1", "mint", "bolt11"])?; - self.http_post(url, &request).await + self.http_post(url, auth_token, &request).await } /// Melt Quote [NUT-05] @@ -190,11 +210,12 @@ impl MintConnector for HttpClient { async fn post_melt_quote( &self, request: MeltQuoteBolt11Request, + auth_token: Option, ) -> Result, Error> { let url = self .mint_url .join_paths(&["v1", "melt", "quote", "bolt11"])?; - self.http_post(url, &request).await + self.http_post(url, auth_token, &request).await } /// Melt Quote Status @@ -202,12 +223,13 @@ impl MintConnector for HttpClient { async fn get_melt_quote_status( &self, quote_id: &str, + auth_token: Option, ) -> Result, Error> { let url = self .mint_url .join_paths(&["v1", "melt", "quote", "bolt11", quote_id])?; - self.http_get(url).await + self.http_get(url, auth_token).await } /// Melt [NUT-05] @@ -216,23 +238,28 @@ impl MintConnector for HttpClient { async fn post_melt( &self, request: MeltBolt11Request, + auth_token: Option, ) -> Result, Error> { let url = self.mint_url.join_paths(&["v1", "melt", "bolt11"])?; - self.http_post(url, &request).await + self.http_post(url, auth_token, &request).await } /// Swap Token [NUT-03] #[instrument(skip(self, swap_request), fields(mint_url = %self.mint_url))] - async fn post_swap(&self, swap_request: SwapRequest) -> Result { + async fn post_swap( + &self, + swap_request: SwapRequest, + auth_token: Option, + ) -> Result { let url = self.mint_url.join_paths(&["v1", "swap"])?; - self.http_post(url, &swap_request).await + self.http_post(url, auth_token, &swap_request).await } /// Get Mint Info [NUT-06] #[instrument(skip(self), fields(mint_url = %self.mint_url))] async fn get_mint_info(&self) -> Result { let url = self.mint_url.join_paths(&["v1", "info"])?; - self.http_get(url).await + self.http_get(url, None).await } /// Spendable check [NUT-07] @@ -240,69 +267,68 @@ impl MintConnector for HttpClient { async fn post_check_state( &self, request: CheckStateRequest, + auth_token: Option, ) -> Result { let url = self.mint_url.join_paths(&["v1", "checkstate"])?; - self.http_post(url, &request).await + self.http_post(url, auth_token, &request).await } /// Restore request [NUT-13] #[instrument(skip(self, request), fields(mint_url = %self.mint_url))] - async fn post_restore(&self, request: RestoreRequest) -> Result { + async fn post_restore( + &self, + request: RestoreRequest, + auth_token: Option, + ) -> Result { let url = self.mint_url.join_paths(&["v1", "restore"])?; - self.http_post(url, &request).await + self.http_post(url, auth_token, &request).await } -} -/// Interface that connects a wallet to a mint. Typically represents an [HttpClient]. -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait MintConnector: Debug { - /// Get Active Mint Keys [NUT-01] - async fn get_mint_keys(&self) -> Result, Error>; - /// Get Keyset Keys [NUT-01] - async fn get_mint_keyset(&self, keyset_id: Id) -> Result; - /// Get Keysets [NUT-02] - async fn get_mint_keysets(&self) -> Result; - /// Mint Quote [NUT-04] - async fn post_mint_quote( - &self, - request: MintQuoteBolt11Request, - ) -> Result, Error>; - /// Mint Quote status - async fn get_mint_quote_status( - &self, - quote_id: &str, - ) -> Result, Error>; - /// Mint Tokens [NUT-04] - async fn post_mint( - &self, - request: MintBolt11Request, - ) -> Result; - /// Melt Quote [NUT-05] - async fn post_melt_quote( - &self, - request: MeltQuoteBolt11Request, - ) -> Result, Error>; - /// Melt Quote Status - async fn get_melt_quote_status( - &self, - quote_id: &str, - ) -> Result, Error>; - /// Melt [NUT-05] - /// [Nut-08] Lightning fee return if outputs defined - async fn post_melt( - &self, - request: MeltBolt11Request, - ) -> Result, Error>; - /// Split Token [NUT-06] - async fn post_swap(&self, request: SwapRequest) -> Result; - /// Get Mint Info [NUT-06] - async fn get_mint_info(&self) -> Result; - /// Spendable check [NUT-07] - async fn post_check_state( + /// Get Active Auth Mint Keys [NUT-XX1] + #[instrument(skip(self), fields(mint_url = %self.mint_url))] + async fn get_mint_blind_auth_keys(&self) -> Result, Error> { + let url = self.mint_url.join_paths(&["v1", "auth", "blind", "keys"])?; + + self.http_get(url, None).await + } + + /// Get Auth Keyset Keys [NUT-XX1] + #[instrument(skip(self), fields(mint_url = %self.mint_url))] + async fn get_mint_blind_auth_keyset(&self, keyset_id: Id) -> Result { + let url = + self.mint_url + .join_paths(&["v1", "auth", "blind", "keys", &keyset_id.to_string()])?; + + let mut keys_response = self.http_get::<_, KeysResponse>(url, None).await?; + + let keyset = keys_response + .keysets + .drain(0..1) + .next() + .ok_or_else(|| Error::UnknownKeySet)?; + + Ok(keyset) + } + + /// Get Auth Keysets [NUT-XX1] + #[instrument(skip(self), fields(mint_url = %self.mint_url))] + async fn get_mint_blind_auth_keysets(&self) -> Result { + let url = self + .mint_url + .join_paths(&["v1", "auth", "blind", "keysets"])?; + + self.http_get(url, None).await + } + + /// Mint Tokens [NUT-XX!] + #[instrument(skip(self, request), fields(mint_url = %self.mint_url))] + async fn post_mint_blind_auth( &self, - request: CheckStateRequest, - ) -> Result; - /// Restore request [NUT-13] - async fn post_restore(&self, request: RestoreRequest) -> Result; + request: MintAuthRequest, + auth_token: AuthToken, + ) -> Result { + let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?; + + self.http_post(url, Some(auth_token), &request).await + } } diff --git a/crates/cdk/src/wallet/mint_connector/mod.rs b/crates/cdk/src/wallet/mint_connector/mod.rs new file mode 100644 index 000000000..04f8eb412 --- /dev/null +++ b/crates/cdk/src/wallet/mint_connector/mod.rs @@ -0,0 +1,107 @@ +//! Wallet client + +use std::fmt::Debug; + +use async_trait::async_trait; + +use super::Error; +use crate::nuts::nutxx1::MintAuthRequest; +use crate::nuts::{ + AuthToken, CheckStateRequest, CheckStateResponse, Id, KeySet, KeysetResponse, + MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, + MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, + RestoreResponse, SwapRequest, SwapResponse, +}; + +mod http_client; + +pub use http_client::HttpClient; + +/// Interface that connects a wallet to a mint. Typically represents an [HttpClient]. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait MintConnector: Debug { + /// Get Active Mint Keys [NUT-01] + async fn get_mint_keys(&self, auth_token: Option) -> Result, Error>; + /// Get Keyset Keys [NUT-01] + async fn get_mint_keyset( + &self, + keyset_id: Id, + auth_token: Option, + ) -> Result; + /// Get Keysets [NUT-02] + async fn get_mint_keysets( + &self, + auth_token: Option, + ) -> Result; + /// Mint Quote [NUT-04] + async fn post_mint_quote( + &self, + request: MintQuoteBolt11Request, + auth_token: Option, + ) -> Result, Error>; + /// Mint Quote status + async fn get_mint_quote_status( + &self, + quote_id: &str, + auth_token: Option, + ) -> Result, Error>; + /// Mint Tokens [NUT-04] + async fn post_mint( + &self, + request: MintBolt11Request, + auth_token: Option, + ) -> Result; + /// Melt Quote [NUT-05] + async fn post_melt_quote( + &self, + request: MeltQuoteBolt11Request, + auth_token: Option, + ) -> Result, Error>; + /// Melt Quote Status + async fn get_melt_quote_status( + &self, + quote_id: &str, + auth_token: Option, + ) -> Result, Error>; + /// Melt [NUT-05] + /// [Nut-08] Lightning fee return if outputs defined + async fn post_melt( + &self, + request: MeltBolt11Request, + auth_token: Option, + ) -> Result, Error>; + /// Split Token [NUT-06] + async fn post_swap( + &self, + request: SwapRequest, + auth_token: Option, + ) -> Result; + /// Get Mint Info [NUT-06] + async fn get_mint_info(&self) -> Result; + /// Spendable check [NUT-07] + async fn post_check_state( + &self, + request: CheckStateRequest, + auth_token: Option, + ) -> Result; + /// Restore request [NUT-13] + async fn post_restore( + &self, + request: RestoreRequest, + auth_token: Option, + ) -> Result; + + /// Get Blind Auth keys + async fn get_mint_blind_auth_keys(&self) -> Result, Error>; + /// Get Blind Auth Keyset + async fn get_mint_blind_auth_keyset(&self, keyset_id: Id) -> Result; + /// Get Blind Auth keysets + async fn get_mint_blind_auth_keysets(&self) -> Result; + /// Post mint blind auth + async fn post_mint_blind_auth( + &self, + request: MintAuthRequest, + auth_token: AuthToken, + ) -> Result; +} diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 68dd25995..d16caf1cb 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -8,10 +8,10 @@ use bitcoin::bip32::Xpriv; use bitcoin::Network; use cdk_common::database::{self, WalletDatabase}; use cdk_common::subscription::Params; -use client::MintConnector; use getrandom::getrandom; pub use multi_mint_wallet::MultiMintWallet; use subscription::{ActiveSubscription, SubscriptionManager}; +use tokio::sync::RwLock; use tracing::instrument; pub use types::{MeltQuote, MintQuote, SendKind}; @@ -23,17 +23,19 @@ use crate::mint_url::MintUrl; use crate::nuts::nut00::token::Token; use crate::nuts::nut17::Kind; use crate::nuts::{ - nut10, CurrencyUnit, Id, Keys, MintInfo, MintQuoteState, PreMintSecrets, Proof, Proofs, - RestoreRequest, SpendingConditions, State, + nut10, AuthRequired, CurrencyUnit, Id, Keys, Method, MintInfo, MintQuoteState, PreMintSecrets, + Proof, Proofs, ProtectedEndpoint, RestoreRequest, RoutePath, SpendingConditions, State, }; use crate::types::ProofInfo; -use crate::{Amount, HttpClient}; +use crate::util::unix_time; +use crate::Amount; +mod auth; mod balance; -pub mod client; mod keysets; mod melt; mod mint; +mod mint_connector; pub mod multi_mint_wallet; mod proofs; mod receive; @@ -43,6 +45,7 @@ mod swap; pub mod util; pub use cdk_common::wallet as types; +pub use mint_connector::{HttpClient, MintConnector}; use crate::nuts::nut00::ProofsMethods; @@ -61,6 +64,10 @@ pub struct Wallet { pub localstore: Arc + Send + Sync>, /// The targeted amount of proofs to have at each size pub target_proof_count: usize, + /// Clear Auth token + pub cat: Arc>>, + /// Protected methods + pub protected_endpoints: Arc>>, xpriv: Xpriv, client: Arc, subscription: SubscriptionManager, @@ -129,7 +136,7 @@ impl Wallet { /// let unit = CurrencyUnit::Sat; /// /// let localstore = WalletMemoryDatabase::default(); - /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None); + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None); /// ``` pub fn new( mint_url: &str, @@ -137,12 +144,15 @@ impl Wallet { localstore: Arc + Send + Sync>, seed: &[u8], target_proof_count: Option, + cat: Option, ) -> Result { let xpriv = Xpriv::new_master(Network::Bitcoin, seed).expect("Could not create master key"); let mint_url = MintUrl::from_str(mint_url)?; let http_client = Arc::new(HttpClient::new(mint_url.clone())); + let cat = Arc::new(RwLock::new(cat)); + Ok(Self { mint_url: mint_url.clone(), unit, @@ -151,6 +161,8 @@ impl Wallet { localstore, xpriv, target_proof_count: target_proof_count.unwrap_or(3), + cat, + protected_endpoints: Arc::new(RwLock::new(HashMap::new())), }) } @@ -163,7 +175,7 @@ impl Wallet { /// Subscribe to events pub async fn subscribe>(&self, query: T) -> ActiveSubscription { self.subscription - .subscribe(self.mint_url.clone(), query.into()) + .subscribe(self.mint_url.clone(), query.into(), Arc::new(self.clone())) .await } @@ -228,21 +240,48 @@ impl Wallet { /// Query mint for current mint information #[instrument(skip(self))] pub async fn get_mint_info(&self) -> Result, Error> { - let mint_info = match self.client.get_mint_info().await { - Ok(mint_info) => Some(mint_info), - Err(err) => { - tracing::warn!("Could not get mint info {}", err); - None - } - }; + match self.client.get_mint_info().await { + Ok(mint_info) => { + // If mint provides time make sure it is accurate + if let Some(mint_unix_time) = mint_info.time { + let current_unix_time = unix_time(); + if current_unix_time.abs_diff(mint_unix_time) > 30 { + tracing::warn!( + "Mint time does match wallet time. Mint: {}, Wallet: {}", + mint_unix_time, + current_unix_time + ); + return Err(Error::MintTimeExceedsTolerance); + } + } - self.localstore - .add_mint(self.mint_url.clone(), mint_info.clone()) - .await?; + self.localstore + .add_mint(self.mint_url.clone(), Some(mint_info.clone())) + .await?; + + let mut protected_endpoints = self.protected_endpoints.write().await; + + if let Some(nutxx_settings) = &mint_info.nuts.nutxx { + for endpoint in nutxx_settings.protected_endpoints.iter() { + protected_endpoints.insert(*endpoint, AuthRequired::Clear); + } + } - tracing::trace!("Mint info updated for {}", self.mint_url); + if let Some(nutxx1_settings) = &mint_info.nuts.nutxx1 { + for endpoint in nutxx1_settings.protected_endpoints.iter() { + protected_endpoints.insert(*endpoint, AuthRequired::Blind); + } + } - Ok(mint_info) + tracing::trace!("Mint info updated for {}", self.mint_url); + + Ok(Some(mint_info)) + } + Err(err) => { + tracing::warn!("Could not get mint info {}", err); + Ok(None) + } + } } /// Get amounts needed to refill proof state @@ -342,7 +381,14 @@ impl Wallet { outputs: premint_secrets.blinded_messages(), }; - let response = self.client.post_restore(restore_request).await?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new(Method::Post, RoutePath::Restore)) + .await?; + + let response = self + .client + .post_restore(restore_request, auth_token) + .await?; if response.signatures.is_empty() { empty_batch += 1; diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index 2ecc83542..f7b5dbdd7 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -5,7 +5,8 @@ use tracing::instrument; use crate::amount::SplitTarget; use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ - CheckStateRequest, Proof, ProofState, Proofs, PublicKey, SpendingConditions, State, + CheckStateRequest, Method, Proof, ProofState, Proofs, ProtectedEndpoint, PublicKey, RoutePath, + SpendingConditions, State, }; use crate::types::ProofInfo; use crate::{Amount, Error, Wallet}; @@ -63,9 +64,13 @@ impl Wallet { pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), Error> { let proof_ys = proofs.ys()?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new(Method::Post, RoutePath::Restore)) + .await?; + let spendable = self .client - .post_check_state(CheckStateRequest { ys: proof_ys }) + .post_check_state(CheckStateRequest { ys: proof_ys }, auth_token) .await? .states; @@ -84,10 +89,15 @@ impl Wallet { /// NUT-07 Check the state of a [`Proof`] with the mint #[instrument(skip(self, proofs))] pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result, Error> { + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new(Method::Post, RoutePath::Restore)) + .await?; + let spendable = self .client - .post_check_state(CheckStateRequest { ys: proofs.ys()? }) + .post_check_state(CheckStateRequest { ys: proofs.ys()? }, auth_token) .await?; + let spent_ys: Vec<_> = spendable .states .iter() diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index aed875a39..4e9186c5f 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -10,7 +10,10 @@ use crate::amount::SplitTarget; use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut10::Kind; -use crate::nuts::{Conditions, Proofs, PublicKey, SecretKey, SigFlag, State, Token}; +use crate::nuts::{ + Conditions, Method, Proofs, ProtectedEndpoint, PublicKey, RoutePath, SecretKey, SigFlag, State, + Token, +}; use crate::types::ProofInfo; use crate::util::hex; use crate::{Amount, Error, Wallet, SECP256K1}; @@ -128,7 +131,14 @@ impl Wallet { } } - let swap_response = self.client.post_swap(pre_swap.swap_request).await?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new(Method::Post, RoutePath::Restore)) + .await?; + + let swap_response = self + .client + .post_swap(pre_swap.swap_request, auth_token) + .await?; // Proof to keep let recv_proofs = construct_proofs( @@ -176,7 +186,7 @@ impl Wallet { /// let unit = CurrencyUnit::Sat; /// /// let localstore = WalletMemoryDatabase::default(); - /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None).unwrap(); /// let token = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJhbW91bnQiOjEsInNlY3JldCI6ImI0ZjVlNDAxMDJhMzhiYjg3NDNiOTkwMzU5MTU1MGYyZGEzZTQxNWEzMzU0OTUyN2M2MmM5ZDc5MGVmYjM3MDUiLCJDIjoiMDIzYmU1M2U4YzYwNTMwZWVhOWIzOTQzZmRhMWEyY2U3MWM3YjNmMGNmMGRjNmQ4NDZmYTc2NWFhZjc3OWZhODFkIiwiaWQiOiIwMDlhMWYyOTMyNTNlNDFlIn1dLCJtaW50IjoiaHR0cHM6Ly90ZXN0bnV0LmNhc2h1LnNwYWNlIn1dLCJ1bml0Ijoic2F0In0="; /// let amount_receive = wallet.receive(token, SplitTarget::default(), &[], &[]).await?; /// Ok(()) @@ -233,7 +243,7 @@ impl Wallet { /// let unit = CurrencyUnit::Sat; /// /// let localstore = WalletMemoryDatabase::default(); - /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap(); + /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None, None).unwrap(); /// let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap(); /// let amount_receive = wallet.receive_raw(&token_raw, SplitTarget::default(), &[], &[]).await?; /// Ok(()) diff --git a/crates/cdk/src/wallet/subscription/http.rs b/crates/cdk/src/wallet/subscription/http.rs index d77a852ed..fc7a32c29 100644 --- a/crates/cdk/src/wallet/subscription/http.rs +++ b/crates/cdk/src/wallet/subscription/http.rs @@ -7,9 +7,13 @@ use tokio::time; use super::WsSubscriptionBody; use crate::nuts::nut17::Kind; -use crate::nuts::{nut01, nut04, nut05, nut07, CheckStateRequest, NotificationPayload}; +use crate::nuts::{ + nut01, nut04, nut05, nut07, CheckStateRequest, Method, NotificationPayload, ProtectedEndpoint, + RoutePath, +}; use crate::pub_sub::SubId; -use crate::wallet::client::MintConnector; +use crate::wallet::MintConnector; +use crate::Wallet; #[derive(Debug, Hash, PartialEq, Eq)] enum UrlType { @@ -78,6 +82,7 @@ pub async fn http_main>( subscriptions: Arc>>, mut new_subscription_recv: mpsc::Receiver, mut on_drop: mpsc::Receiver, + wallet: Arc, ) { let mut interval = time::interval(Duration::from_secs(2)); let mut subscribed_to = HashMap::, _, AnyState)>::new(); @@ -93,7 +98,14 @@ pub async fn http_main>( tracing::debug!("Polling: {:?}", url); match url { UrlType::Mint(id) => { - let response = http_client.get_mint_quote_status(id).await; + + let auth_token = wallet + .get_auth_for_request(&ProtectedEndpoint::new( + Method::Post, + RoutePath::MeltBolt11, + )) + .await.unwrap(); + let response = http_client.get_mint_quote_status(id, auth_token).await; if let Ok(response) = response { if *last_state == AnyState::MintQuoteState(response.state) { continue; @@ -105,7 +117,14 @@ pub async fn http_main>( } } UrlType::Melt(id) => { - let response = http_client.get_melt_quote_status(id).await; + let auth_token = wallet + .get_auth_for_request(&ProtectedEndpoint::new( + Method::Post, + RoutePath::MeltQuoteBolt11 + )) + .await.unwrap(); + + let response = http_client.get_melt_quote_status(id, auth_token).await; if let Ok(response) = response { if *last_state == AnyState::MeltQuoteState(response.state) { continue; @@ -117,9 +136,16 @@ pub async fn http_main>( } } UrlType::PublicKey(id) => { + let auth_token = wallet + .get_auth_for_request(&ProtectedEndpoint::new( + Method::Post, + RoutePath::MeltQuoteBolt11 + )) + .await.unwrap(); let responses = http_client.post_check_state(CheckStateRequest { ys: vec![*id], - }).await; + }, auth_token + ).await; if let Ok(mut responses) = responses { let response = if let Some(state) = responses.states.pop() { state diff --git a/crates/cdk/src/wallet/subscription/mod.rs b/crates/cdk/src/wallet/subscription/mod.rs index 31544dd1a..a04a846e9 100644 --- a/crates/cdk/src/wallet/subscription/mod.rs +++ b/crates/cdk/src/wallet/subscription/mod.rs @@ -14,9 +14,10 @@ use tokio::sync::{mpsc, RwLock}; use tokio::task::JoinHandle; use tracing::error; +use super::Wallet; use crate::mint_url::MintUrl; use crate::pub_sub::SubId; -use crate::wallet::client::MintConnector; +use crate::wallet::MintConnector; mod http; #[cfg(all( @@ -59,7 +60,12 @@ impl SubscriptionManager { } /// Subscribe to updates from a mint server with a given filter - pub async fn subscribe(&self, mint_url: MintUrl, filter: Params) -> ActiveSubscription { + pub async fn subscribe( + &self, + mint_url: MintUrl, + filter: Params, + wallet: Arc, + ) -> ActiveSubscription { let subscription_clients = self.all_connections.read().await; let id = filter.id.clone(); if let Some(subscription_client) = subscription_clients.get(&mint_url) { @@ -94,8 +100,12 @@ impl SubscriptionManager { ); let mut subscription_clients = self.all_connections.write().await; - let subscription_client = - SubscriptionClient::new(mint_url.clone(), self.http_client.clone(), is_ws_support); + let subscription_client = SubscriptionClient::new( + mint_url.clone(), + self.http_client.clone(), + is_ws_support, + wallet, + ); let (on_drop_notif, receiver) = subscription_client.subscribe(filter).await; subscription_clients.insert(mint_url, subscription_client); @@ -179,6 +189,7 @@ impl SubscriptionClient { url: MintUrl, http_client: Arc, prefer_ws_method: bool, + wallet: Arc, ) -> Self { let subscriptions = Arc::new(RwLock::new(HashMap::new())); let (new_subscription_notif, new_subscription_recv) = mpsc::channel(100); @@ -195,6 +206,7 @@ impl SubscriptionClient { subscriptions, new_subscription_recv, on_drop_recv, + wallet, )), } } @@ -207,6 +219,7 @@ impl SubscriptionClient { subscriptions: Arc>>, new_subscription_recv: mpsc::Receiver, on_drop_recv: mpsc::Receiver, + wallet: Arc, ) -> JoinHandle<()> { #[cfg(any( feature = "http_subscription", @@ -218,6 +231,7 @@ impl SubscriptionClient { subscriptions, new_subscription_recv, on_drop_recv, + wallet, ); #[cfg(all( @@ -232,6 +246,7 @@ impl SubscriptionClient { subscriptions, new_subscription_recv, on_drop_recv, + wallet, ) } else { Self::http_worker( @@ -239,6 +254,7 @@ impl SubscriptionClient { subscriptions, new_subscription_recv, on_drop_recv, + wallet, ) } } @@ -268,6 +284,7 @@ impl SubscriptionClient { subscriptions: Arc>>, new_subscription_recv: mpsc::Receiver, on_drop: mpsc::Receiver, + wallet: Arc, ) -> JoinHandle<()> { let http_worker = http::http_main( vec![], @@ -275,6 +292,7 @@ impl SubscriptionClient { subscriptions, new_subscription_recv, on_drop, + wallet, ); #[cfg(target_arch = "wasm32")] @@ -302,6 +320,7 @@ impl SubscriptionClient { subscriptions: Arc>>, new_subscription_recv: mpsc::Receiver, on_drop: mpsc::Receiver, + wallet: Arc, ) -> JoinHandle<()> { tokio::spawn(ws::ws_main( http_client, @@ -309,6 +328,7 @@ impl SubscriptionClient { subscriptions, new_subscription_recv, on_drop, + wallet, )) } } diff --git a/crates/cdk/src/wallet/subscription/ws.rs b/crates/cdk/src/wallet/subscription/ws.rs index f90cbe06e..6490ed4af 100644 --- a/crates/cdk/src/wallet/subscription/ws.rs +++ b/crates/cdk/src/wallet/subscription/ws.rs @@ -13,7 +13,8 @@ use super::http::http_main; use super::WsSubscriptionBody; use crate::mint_url::MintUrl; use crate::pub_sub::SubId; -use crate::wallet::client::MintConnector; +use crate::wallet::MintConnector; +use crate::Wallet; const MAX_ATTEMPT_FALLBACK_HTTP: usize = 10; @@ -23,6 +24,7 @@ async fn fallback_to_http>( subscriptions: Arc>>, new_subscription_recv: mpsc::Receiver, on_drop: mpsc::Receiver, + wallet: Arc, ) { http_main( initial_state, @@ -30,6 +32,7 @@ async fn fallback_to_http>( subscriptions, new_subscription_recv, on_drop, + wallet, ) .await } @@ -42,6 +45,7 @@ pub async fn ws_main( subscriptions: Arc>>, mut new_subscription_recv: mpsc::Receiver, mut on_drop: mpsc::Receiver, + wallet: Arc, ) { let url = mint_url .join_paths(&["v1", "ws"]) @@ -77,6 +81,7 @@ pub async fn ws_main( subscriptions, new_subscription_recv, on_drop, + wallet, ) .await; } @@ -176,6 +181,7 @@ pub async fn ws_main( subscriptions, new_subscription_recv, on_drop, + wallet ).await; } } diff --git a/crates/cdk/src/wallet/swap.rs b/crates/cdk/src/wallet/swap.rs index e50c986d0..e794f8d00 100644 --- a/crates/cdk/src/wallet/swap.rs +++ b/crates/cdk/src/wallet/swap.rs @@ -4,7 +4,8 @@ use crate::amount::SplitTarget; use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ - nut10, PreMintSecrets, PreSwap, Proofs, PublicKey, SpendingConditions, State, SwapRequest, + nut10, Method, PreMintSecrets, PreSwap, Proofs, ProtectedEndpoint, PublicKey, RoutePath, + SpendingConditions, State, SwapRequest, }; use crate::types::ProofInfo; use crate::{Amount, Error, Wallet}; @@ -33,7 +34,14 @@ impl Wallet { ) .await?; - let swap_response = self.client.post_swap(pre_swap.swap_request).await?; + let auth_token = self + .get_auth_for_request(&ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11)) + .await?; + + let swap_response = self + .client + .post_swap(pre_swap.swap_request, auth_token) + .await?; let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id; diff --git a/flake.lock b/flake.lock index 06b08ea87..cf5c9c229 100644 --- a/flake.lock +++ b/flake.lock @@ -20,7 +20,7 @@ "nixpkgs": [ "nixpkgs" ], - "rust-analyzer-src": [] + "rust-analyzer-src": "rust-analyzer-src" }, "locked": { "lastModified": 1736145356, @@ -153,6 +153,23 @@ "rust-overlay": "rust-overlay" } }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1736009406, + "narHash": "sha256-5P+kK7S64/Mg8NrGQ3ScqoRW7vJAKzoeGJCFhEbldN0=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "6725e046df7493c1047e115ebc7180fb06416038", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, "rust-overlay": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index 0ecd721e4..8b121a3f2 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,6 @@ fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; - inputs.rust-analyzer-src.follows = ""; }; flake-utils.url = "github:numtide/flake-utils"; @@ -51,7 +50,7 @@ # latest stable stable_toolchain = pkgs.rust-bin.stable."1.83.0".default.override { targets = [ "wasm32-unknown-unknown" ]; # wasm - extensions = [ "rustfmt" "clippy" "rust-analyzer" ]; + extensions = [ "rustfmt" "clippy" "rust-src" ]; }; # MSRV stable @@ -67,7 +66,8 @@ # Nightly used for formatting nightly_toolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { - extensions = [ "rustfmt" "clippy" "rust-analyzer" ]; + targets = [ "wasm32-unknown-unknown" ]; # wasm + extensions = [ "rustfmt" "clippy" "rust-src" ]; }); # Common inputs @@ -85,6 +85,10 @@ clightning bitcoind sqlx-cli + typos-lsp + + fenix.packages.${system}.rust-analyzer + # Needed for github ci libz diff --git a/justfile b/justfile index 4e36977ed..bd8a129cb 100644 --- a/justfile +++ b/justfile @@ -3,12 +3,12 @@ import "./misc/test.just" alias b := build alias c := check -alias t := test +alias t := dtest default: @just --list -final-check: typos format clippy test +final-check: typos format clippy dtest # run `cargo build` on everything build *ARGS="--workspace --all-targets": @@ -38,8 +38,8 @@ format: cargo fmt --all nixpkgs-fmt $(echo **.nix) -# run tests -test: build +# run doc tests +dtest: build #!/usr/bin/env bash set -euo pipefail if [ ! -f Cargo.toml ]; then @@ -47,6 +47,14 @@ test: build fi cargo test --lib +test-all db: + #!/usr/bin/env bash + just dtest + cargo test -p cdk-integration-tests --test mint + ./misc/itests.sh "{{db}}" + ./misc/fake_itests.sh "{{db}}" + + # run `cargo clippy` on everything clippy *ARGS="--locked --offline --workspace --all-targets": cargo clippy {{ARGS}}