mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 21:55:03 +01:00
Clear and Blind Auth (#510)
* feat: auth * chore: corret error codes * chore: corret error codes * fix: feature auth in cdk-axum * refactor: auth logging * feat: include dleq in auth proof * feat: mint max auth proofs * chore: clippy
This commit is contained in:
70
.github/workflows/ci.yml
vendored
70
.github/workflows/ci.yml
vendored
@@ -84,16 +84,26 @@ jobs:
|
|||||||
-p cashu --no-default-features --features wallet,
|
-p cashu --no-default-features --features wallet,
|
||||||
-p cashu --no-default-features --features mint,
|
-p cashu --no-default-features --features mint,
|
||||||
-p cashu --no-default-features --features "mint swagger",
|
-p cashu --no-default-features --features "mint swagger",
|
||||||
|
-p cashu --no-default-features --features auth,
|
||||||
|
-p cashu --no-default-features --features "mint auth",
|
||||||
|
-p cashu --no-default-features --features "wallet auth",
|
||||||
-p cdk-common,
|
-p cdk-common,
|
||||||
-p cdk-common --no-default-features,
|
-p cdk-common --no-default-features,
|
||||||
-p cdk-common --no-default-features --features wallet,
|
-p cdk-common --no-default-features --features wallet,
|
||||||
-p cdk-common --no-default-features --features mint,
|
-p cdk-common --no-default-features --features mint,
|
||||||
-p cdk-common --no-default-features --features "mint swagger",
|
-p cdk-common --no-default-features --features "mint swagger",
|
||||||
|
-p cdk-common --no-default-features --features "auth",
|
||||||
|
-p cdk-common --no-default-features --features "mint auth",
|
||||||
|
-p cdk-common --no-default-features --features "wallet auth",
|
||||||
-p cdk,
|
-p cdk,
|
||||||
-p cdk --no-default-features,
|
-p cdk --no-default-features,
|
||||||
-p cdk --no-default-features --features wallet,
|
-p cdk --no-default-features --features wallet,
|
||||||
-p cdk --no-default-features --features mint,
|
-p cdk --no-default-features --features mint,
|
||||||
-p cdk --no-default-features --features "mint swagger",
|
-p cdk --no-default-features --features "mint swagger",
|
||||||
|
-p cdk --no-default-features --features auth,
|
||||||
|
-p cdk --features auth,
|
||||||
|
-p cdk --no-default-features --features "auth mint",
|
||||||
|
-p cdk --no-default-features --features "auth wallet",
|
||||||
-p cdk-redb,
|
-p cdk-redb,
|
||||||
-p cdk-sqlite,
|
-p cdk-sqlite,
|
||||||
-p cdk-sqlite --features sqlcipher,
|
-p cdk-sqlite --features sqlcipher,
|
||||||
@@ -101,6 +111,7 @@ jobs:
|
|||||||
-p cdk-axum --no-default-features --features swagger,
|
-p cdk-axum --no-default-features --features swagger,
|
||||||
-p cdk-axum --no-default-features --features redis,
|
-p cdk-axum --no-default-features --features redis,
|
||||||
-p cdk-axum --no-default-features --features "redis swagger",
|
-p cdk-axum --no-default-features --features "redis swagger",
|
||||||
|
-p cdk-axum --no-default-features --features "auth redis",
|
||||||
-p cdk-axum,
|
-p cdk-axum,
|
||||||
-p cdk-cln,
|
-p cdk-cln,
|
||||||
-p cdk-lnd,
|
-p cdk-lnd,
|
||||||
@@ -126,6 +137,8 @@ jobs:
|
|||||||
--bin cdk-mintd --no-default-features --features "swagger lnd",
|
--bin cdk-mintd --no-default-features --features "swagger lnd",
|
||||||
--bin cdk-mintd --no-default-features --features "swagger cln",
|
--bin cdk-mintd --no-default-features --features "swagger cln",
|
||||||
--bin cdk-mintd --no-default-features --features "swagger lnbits",
|
--bin cdk-mintd --no-default-features --features "swagger lnbits",
|
||||||
|
--bin cdk-mintd --no-default-features --features "auth lnd",
|
||||||
|
--bin cdk-mintd --no-default-features --features "auth cln",
|
||||||
--bin cdk-mint-cli,
|
--bin cdk-mint-cli,
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
@@ -142,11 +155,11 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: nix develop -i -L .#stable --command cargo test ${{ matrix.build-args }}
|
run: nix develop -i -L .#stable --command cargo test ${{ matrix.build-args }}
|
||||||
|
|
||||||
itest:
|
regtest-itest:
|
||||||
name: "Integration regtest tests"
|
name: "Integration regtest tests"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
needs: [pre-commit-checks, clippy, pure-itest, fake-wallet-itest]
|
needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
build-args:
|
build-args:
|
||||||
@@ -167,13 +180,11 @@ jobs:
|
|||||||
uses: DeterminateSystems/magic-nix-cache-action@v6
|
uses: DeterminateSystems/magic-nix-cache-action@v6
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Clippy
|
|
||||||
run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: nix develop -i -L .#stable --command just itest ${{ matrix.database }}
|
run: nix develop -i -L .#stable --command just itest ${{ matrix.database }}
|
||||||
|
|
||||||
fake-wallet-itest:
|
fake-mint-itest:
|
||||||
name: "Integration fake wallet tests"
|
name: "Integration fake mint tests"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
needs: [pre-commit-checks, clippy]
|
needs: [pre-commit-checks, clippy]
|
||||||
@@ -198,8 +209,8 @@ jobs:
|
|||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Clippy
|
- 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
|
- name: Test fake auth mint
|
||||||
run: nix develop -i -L .#stable --command just fake-mint-itest ${{ matrix.database }}
|
run: nix develop -i -L .#stable --command just fake-mint-itest ${{ matrix.database }}
|
||||||
|
|
||||||
pure-itest:
|
pure-itest:
|
||||||
@@ -224,7 +235,7 @@ jobs:
|
|||||||
name: "Payment processor tests"
|
name: "Payment processor tests"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
needs: [pre-commit-checks, clippy, pure-itest, fake-wallet-itest, itest]
|
needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest, regtest-itest]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ln:
|
ln:
|
||||||
@@ -256,7 +267,10 @@ jobs:
|
|||||||
[
|
[
|
||||||
-p cashu --no-default-features --features "wallet mint",
|
-p cashu --no-default-features --features "wallet mint",
|
||||||
-p cdk-common --no-default-features --features "wallet mint",
|
-p cdk-common --no-default-features --features "wallet mint",
|
||||||
-p cdk --no-default-features --features "mint mint",
|
-p cdk,
|
||||||
|
-p cdk --no-default-features --features "mint auth",
|
||||||
|
-p cdk --no-default-features --features "wallet auth",
|
||||||
|
-p cdk --no-default-features --features "http_subscription",
|
||||||
-p cdk-axum,
|
-p cdk-axum,
|
||||||
-p cdk-axum --no-default-features --features redis,
|
-p cdk-axum --no-default-features --features redis,
|
||||||
-p cdk-lnbits,
|
-p cdk-lnbits,
|
||||||
@@ -339,3 +353,39 @@ jobs:
|
|||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: Build cdk wasm
|
- name: Build cdk wasm
|
||||||
run: nix develop -i -L ".#${{ matrix.rust }}" --command cargo build ${{ matrix.build-args }} --target ${{ matrix.target }}
|
run: nix develop -i -L ".#${{ matrix.rust }}" --command cargo build ${{ matrix.build-args }} --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
fake-mint-auth-itest:
|
||||||
|
name: "Integration fake mint auth tests"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
database:
|
||||||
|
[
|
||||||
|
REDB,
|
||||||
|
SQLITE,
|
||||||
|
]
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@v11
|
||||||
|
- name: Nix Cache
|
||||||
|
uses: DeterminateSystems/magic-nix-cache-action@v6
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Start Keycloak with Backup
|
||||||
|
run: |
|
||||||
|
docker compose -f misc/keycloak/docker-compose-recover.yml up -d
|
||||||
|
until docker logs $(docker ps -q --filter "ancestor=quay.io/keycloak/keycloak:25.0.6") | grep "Keycloak 25.0.6 on JVM (powered by Quarkus 3.8.5) started"; do sleep 1; done
|
||||||
|
|
||||||
|
- name: Verify Keycloak Import
|
||||||
|
run: |
|
||||||
|
docker logs $(docker ps -q --filter "ancestor=quay.io/keycloak/keycloak:25.0.6") | grep "Imported"
|
||||||
|
- name: Test fake auth mint
|
||||||
|
run: nix develop -i -L .#stable --command just fake-auth-mint-itest ${{ matrix.database }} http://127.0.0.1:8080/realms/cdk-test-realm/.well-known/openid-configuration
|
||||||
|
- name: Stop and clean up Docker Compose
|
||||||
|
run: |
|
||||||
|
docker compose -f misc/keycloak/docker-compose-recover.yml down
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,3 +10,5 @@ config.toml
|
|||||||
result
|
result
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
.aider*
|
.aider*
|
||||||
|
**/postgres_data/
|
||||||
|
**/.env
|
||||||
|
|||||||
@@ -3,5 +3,8 @@ extend-ignore-re = [
|
|||||||
# Ignore cashu tokens
|
# Ignore cashu tokens
|
||||||
"cashuA[A-Za-z0-9-_]+",
|
"cashuA[A-Za-z0-9-_]+",
|
||||||
"cashuB[A-Za-z0-9-_]+",
|
"cashuB[A-Za-z0-9-_]+",
|
||||||
"casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9"
|
"casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9",
|
||||||
|
"autheticator",
|
||||||
|
"Gam",
|
||||||
|
"flate2"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ async-trait = "0.1"
|
|||||||
axum = { version = "0.8.1", features = ["ws"] }
|
axum = { version = "0.8.1", features = ["ws"] }
|
||||||
bitcoin = { version = "0.32.2", features = ["base64", "serde", "rand", "rand-std"] }
|
bitcoin = { version = "0.32.2", features = ["base64", "serde", "rand", "rand-std"] }
|
||||||
bip39 = { version = "2.0", features = ["rand"] }
|
bip39 = { version = "2.0", features = ["rand"] }
|
||||||
|
jsonwebtoken = "9.2.0"
|
||||||
cashu = { path = "./crates/cashu", version = "=0.7.1" }
|
cashu = { path = "./crates/cashu", version = "=0.7.1" }
|
||||||
cdk = { path = "./crates/cdk", default-features = false, version = "=0.7.2" }
|
cdk = { path = "./crates/cdk", default-features = false, version = "=0.7.2" }
|
||||||
cdk-common = { path = "./crates/cdk-common", default-features = false, version = "=0.7.1" }
|
cdk-common = { path = "./crates/cdk-common", default-features = false, version = "=0.7.1" }
|
||||||
@@ -64,6 +65,7 @@ reqwest = { version = "0.12", default-features = false, features = [
|
|||||||
once_cell = "1.20.2"
|
once_cell = "1.20.2"
|
||||||
instant = { version = "0.1", default-features = false }
|
instant = { version = "0.1", default-features = false }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
regex = "1"
|
||||||
home = "0.5.5"
|
home = "0.5.5"
|
||||||
tonic = { version = "0.12.3", features = [
|
tonic = { version = "0.12.3", features = [
|
||||||
"channel",
|
"channel",
|
||||||
@@ -72,6 +74,8 @@ tonic = { version = "0.12.3", features = [
|
|||||||
] }
|
] }
|
||||||
prost = "0.13.1"
|
prost = "0.13.1"
|
||||||
tonic-build = "0.12"
|
tonic-build = "0.12"
|
||||||
|
strum = "0.27.1"
|
||||||
|
strum_macros = "0.27.1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ rust-version = "1.75.0" # MSRV
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["mint", "wallet"]
|
default = ["mint", "wallet", "auth"]
|
||||||
swagger = ["dep:utoipa"]
|
swagger = ["dep:utoipa"]
|
||||||
mint = ["dep:uuid"]
|
mint = ["dep:uuid"]
|
||||||
wallet = []
|
wallet = []
|
||||||
|
auth = ["dep:strum", "dep:strum_macros", "dep:regex"]
|
||||||
bench = []
|
bench = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -30,6 +31,9 @@ url.workspace = true
|
|||||||
utoipa = { workspace = true, optional = true }
|
utoipa = { workspace = true, optional = true }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
|
regex = { workspace = true, optional = true }
|
||||||
|
strum = { workspace = true, optional = true }
|
||||||
|
strum_macros = { workspace = true, optional = true }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
instant = { workspace = true, features = ["wasm-bindgen", "inaccurate"] }
|
instant = { workspace = true, features = ["wasm-bindgen", "inaccurate"] }
|
||||||
|
|||||||
8
crates/cashu/src/nuts/auth/mod.rs
Normal file
8
crates/cashu/src/nuts/auth/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod nut21;
|
||||||
|
pub mod nut22;
|
||||||
|
|
||||||
|
pub use nut21::{Method, ProtectedEndpoint, RoutePath, Settings as ClearAuthSettings};
|
||||||
|
pub use nut22::{
|
||||||
|
AuthProof, AuthRequired, AuthToken, BlindAuthToken, MintAuthRequest,
|
||||||
|
Settings as BlindAuthSettings,
|
||||||
|
};
|
||||||
409
crates/cashu/src/nuts/auth/nut21.rs
Normal file
409
crates/cashu/src/nuts/auth/nut21.rs
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
//! 21 Clear Auth
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use strum_macros::EnumIter;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// NUT21 Error
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Invalid regex pattern
|
||||||
|
#[error("Invalid regex pattern: {0}")]
|
||||||
|
InvalidRegex(#[from] regex::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear Auth Settings
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
pub struct Settings {
|
||||||
|
/// Openid discovery
|
||||||
|
pub openid_discovery: String,
|
||||||
|
/// Client ID
|
||||||
|
pub client_id: String,
|
||||||
|
/// Protected endpoints
|
||||||
|
pub protected_endpoints: Vec<ProtectedEndpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
/// Create new [`Settings`]
|
||||||
|
pub fn new(
|
||||||
|
openid_discovery: String,
|
||||||
|
client_id: String,
|
||||||
|
protected_endpoints: Vec<ProtectedEndpoint>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
openid_discovery,
|
||||||
|
client_id,
|
||||||
|
protected_endpoints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom deserializer for Settings to expand regex patterns in protected endpoints
|
||||||
|
impl<'de> Deserialize<'de> for Settings {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
// Define a temporary struct to deserialize the raw data
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RawSettings {
|
||||||
|
openid_discovery: String,
|
||||||
|
client_id: String,
|
||||||
|
protected_endpoints: Vec<RawProtectedEndpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RawProtectedEndpoint {
|
||||||
|
method: Method,
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize into the temporary struct
|
||||||
|
let raw = RawSettings::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
// Process protected endpoints, expanding regex patterns if present
|
||||||
|
let mut protected_endpoints = HashSet::new();
|
||||||
|
|
||||||
|
for raw_endpoint in raw.protected_endpoints {
|
||||||
|
let expanded_paths = matching_route_paths(&raw_endpoint.path).map_err(|e| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"Invalid regex pattern '{}': {}",
|
||||||
|
raw_endpoint.path, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for path in expanded_paths {
|
||||||
|
protected_endpoints.insert(ProtectedEndpoint::new(raw_endpoint.method, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the final Settings struct
|
||||||
|
Ok(Settings {
|
||||||
|
openid_discovery: raw.openid_discovery,
|
||||||
|
client_id: raw.client_id,
|
||||||
|
protected_endpoints: protected_endpoints.into_iter().collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List of the methods and paths that are protected
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
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")]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
pub enum Method {
|
||||||
|
/// Get
|
||||||
|
Get,
|
||||||
|
/// POST
|
||||||
|
Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route path
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn matching_route_paths(pattern: &str) -> Result<Vec<RoutePath>, Error> {
|
||||||
|
let regex = Regex::from_str(pattern)?;
|
||||||
|
|
||||||
|
Ok(RoutePath::iter()
|
||||||
|
.filter(|path| regex.is_match(&path.to_string()))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RoutePath {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
// Use serde to serialize to a JSON string, then extract the value without quotes
|
||||||
|
let json_str = match serde_json::to_string(self) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return write!(f, "<error>"),
|
||||||
|
};
|
||||||
|
// Remove the quotes from the JSON string
|
||||||
|
let path = json_str.trim_matches('"');
|
||||||
|
write!(f, "{}", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matching_route_paths_all() {
|
||||||
|
// Regex that matches all paths
|
||||||
|
let paths = matching_route_paths(".*").unwrap();
|
||||||
|
|
||||||
|
// Should match all variants
|
||||||
|
assert_eq!(paths.len(), RoutePath::iter().count());
|
||||||
|
|
||||||
|
// Verify all variants are included
|
||||||
|
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
|
||||||
|
assert!(paths.contains(&RoutePath::MintBolt11));
|
||||||
|
assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
|
||||||
|
assert!(paths.contains(&RoutePath::MeltBolt11));
|
||||||
|
assert!(paths.contains(&RoutePath::Swap));
|
||||||
|
assert!(paths.contains(&RoutePath::Checkstate));
|
||||||
|
assert!(paths.contains(&RoutePath::Restore));
|
||||||
|
assert!(paths.contains(&RoutePath::MintBlindAuth));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matching_route_paths_mint_only() {
|
||||||
|
// Regex that matches only mint paths
|
||||||
|
let paths = matching_route_paths("^/v1/mint/.*").unwrap();
|
||||||
|
|
||||||
|
// Should match only mint paths
|
||||||
|
assert_eq!(paths.len(), 2);
|
||||||
|
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
|
||||||
|
assert!(paths.contains(&RoutePath::MintBolt11));
|
||||||
|
|
||||||
|
// Should not match other paths
|
||||||
|
assert!(!paths.contains(&RoutePath::MeltQuoteBolt11));
|
||||||
|
assert!(!paths.contains(&RoutePath::MeltBolt11));
|
||||||
|
assert!(!paths.contains(&RoutePath::Swap));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matching_route_paths_quote_only() {
|
||||||
|
// Regex that matches only quote paths
|
||||||
|
let paths = matching_route_paths(".*/quote/.*").unwrap();
|
||||||
|
|
||||||
|
// Should match only quote paths
|
||||||
|
assert_eq!(paths.len(), 2);
|
||||||
|
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
|
||||||
|
assert!(paths.contains(&RoutePath::MeltQuoteBolt11));
|
||||||
|
|
||||||
|
// Should not match non-quote paths
|
||||||
|
assert!(!paths.contains(&RoutePath::MintBolt11));
|
||||||
|
assert!(!paths.contains(&RoutePath::MeltBolt11));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matching_route_paths_no_match() {
|
||||||
|
// Regex that matches nothing
|
||||||
|
let paths = matching_route_paths("/nonexistent/path").unwrap();
|
||||||
|
|
||||||
|
// Should match nothing
|
||||||
|
assert!(paths.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matching_route_paths_quote_bolt11_only() {
|
||||||
|
// Regex that matches only quote paths
|
||||||
|
let paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
|
||||||
|
|
||||||
|
// Should match only quote paths
|
||||||
|
assert_eq!(paths.len(), 1);
|
||||||
|
assert!(paths.contains(&RoutePath::MintQuoteBolt11));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matching_route_paths_invalid_regex() {
|
||||||
|
// Invalid regex pattern
|
||||||
|
let result = matching_route_paths("(unclosed parenthesis");
|
||||||
|
|
||||||
|
// Should return an error for invalid regex
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), Error::InvalidRegex(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_route_path_to_string() {
|
||||||
|
// Test that to_string() returns the correct path strings
|
||||||
|
assert_eq!(
|
||||||
|
RoutePath::MintQuoteBolt11.to_string(),
|
||||||
|
"/v1/mint/quote/bolt11"
|
||||||
|
);
|
||||||
|
assert_eq!(RoutePath::MintBolt11.to_string(), "/v1/mint/bolt11");
|
||||||
|
assert_eq!(
|
||||||
|
RoutePath::MeltQuoteBolt11.to_string(),
|
||||||
|
"/v1/melt/quote/bolt11"
|
||||||
|
);
|
||||||
|
assert_eq!(RoutePath::MeltBolt11.to_string(), "/v1/melt/bolt11");
|
||||||
|
assert_eq!(RoutePath::Swap.to_string(), "/v1/swap");
|
||||||
|
assert_eq!(RoutePath::Checkstate.to_string(), "/v1/checkstate");
|
||||||
|
assert_eq!(RoutePath::Restore.to_string(), "/v1/restore");
|
||||||
|
assert_eq!(RoutePath::MintBlindAuth.to_string(), "/v1/auth/blind/mint");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_direct_paths() {
|
||||||
|
let json = r#"{
|
||||||
|
"openid_discovery": "https://example.com/.well-known/openid-configuration",
|
||||||
|
"client_id": "client123",
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/v1/mint/bolt11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/swap"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let settings: Settings = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
settings.openid_discovery,
|
||||||
|
"https://example.com/.well-known/openid-configuration"
|
||||||
|
);
|
||||||
|
assert_eq!(settings.client_id, "client123");
|
||||||
|
assert_eq!(settings.protected_endpoints.len(), 2);
|
||||||
|
|
||||||
|
// Check that both paths are included
|
||||||
|
let paths = settings
|
||||||
|
.protected_endpoints
|
||||||
|
.iter()
|
||||||
|
.map(|ep| (ep.method, ep.path))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert!(paths.contains(&(Method::Get, RoutePath::MintBolt11)));
|
||||||
|
assert!(paths.contains(&(Method::Post, RoutePath::Swap)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_with_regex() {
|
||||||
|
let json = r#"{
|
||||||
|
"openid_discovery": "https://example.com/.well-known/openid-configuration",
|
||||||
|
"client_id": "client123",
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "^/v1/mint/.*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/swap"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let settings: Settings = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
settings.openid_discovery,
|
||||||
|
"https://example.com/.well-known/openid-configuration"
|
||||||
|
);
|
||||||
|
assert_eq!(settings.client_id, "client123");
|
||||||
|
assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
|
||||||
|
|
||||||
|
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let deserlized_protected = settings.protected_endpoints.into_iter().collect();
|
||||||
|
|
||||||
|
assert_eq!(expected_protected, deserlized_protected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_invalid_regex() {
|
||||||
|
let json = r#"{
|
||||||
|
"openid_discovery": "https://example.com/.well-known/openid-configuration",
|
||||||
|
"client_id": "client123",
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "(unclosed parenthesis"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let result = serde_json::from_str::<Settings>(json);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_exact_path_match() {
|
||||||
|
let json = r#"{
|
||||||
|
"openid_discovery": "https://example.com/.well-known/openid-configuration",
|
||||||
|
"client_id": "client123",
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/v1/mint/quote/bolt11"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let settings: Settings = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(settings.protected_endpoints.len(), 1);
|
||||||
|
assert_eq!(settings.protected_endpoints[0].method, Method::Get);
|
||||||
|
assert_eq!(
|
||||||
|
settings.protected_endpoints[0].path,
|
||||||
|
RoutePath::MintQuoteBolt11
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_all_paths() {
|
||||||
|
let json = r#"{
|
||||||
|
"openid_discovery": "https://example.com/.well-known/openid-configuration",
|
||||||
|
"client_id": "client123",
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": ".*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let settings: Settings = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
settings.protected_endpoints.len(),
|
||||||
|
RoutePath::iter().count()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
369
crates/cashu/src/nuts/auth/nut22.rs
Normal file
369
crates/cashu/src/nuts/auth/nut22.rs
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
//! 22 Blind Auth
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use bitcoin::base64::engine::general_purpose;
|
||||||
|
use bitcoin::base64::Engine;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::nut21::ProtectedEndpoint;
|
||||||
|
use crate::dhke::hash_to_curve;
|
||||||
|
use crate::secret::Secret;
|
||||||
|
use crate::util::hex;
|
||||||
|
use crate::{BlindedMessage, Id, Proof, ProofDleq, PublicKey};
|
||||||
|
|
||||||
|
/// NUT22 Error
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Invalid Prefix
|
||||||
|
#[error("Invalid prefix")]
|
||||||
|
InvalidPrefix,
|
||||||
|
/// Dleq proof not included
|
||||||
|
#[error("Dleq Proof not included for auth proof")]
|
||||||
|
DleqProofNotIncluded,
|
||||||
|
/// 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)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
|
pub struct Settings {
|
||||||
|
/// Max number of blind auth tokens that can be minted per request
|
||||||
|
pub bat_max_mint: u64,
|
||||||
|
/// Protected endpoints
|
||||||
|
pub protected_endpoints: Vec<ProtectedEndpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
/// Create new [`Settings`]
|
||||||
|
pub fn new(bat_max_mint: u64, protected_endpoints: Vec<ProtectedEndpoint>) -> Self {
|
||||||
|
Self {
|
||||||
|
bat_max_mint,
|
||||||
|
protected_endpoints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom deserializer for Settings to expand regex patterns in protected endpoints
|
||||||
|
impl<'de> Deserialize<'de> for Settings {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use super::nut21::matching_route_paths;
|
||||||
|
|
||||||
|
// Define a temporary struct to deserialize the raw data
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RawSettings {
|
||||||
|
bat_max_mint: u64,
|
||||||
|
protected_endpoints: Vec<RawProtectedEndpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RawProtectedEndpoint {
|
||||||
|
method: super::nut21::Method,
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize into the temporary struct
|
||||||
|
let raw = RawSettings::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
// Process protected endpoints, expanding regex patterns if present
|
||||||
|
let mut protected_endpoints = HashSet::new();
|
||||||
|
|
||||||
|
for raw_endpoint in raw.protected_endpoints {
|
||||||
|
let expanded_paths = matching_route_paths(&raw_endpoint.path).map_err(|e| {
|
||||||
|
serde::de::Error::custom(format!(
|
||||||
|
"Invalid regex pattern '{}': {}",
|
||||||
|
raw_endpoint.path, e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for path in expanded_paths {
|
||||||
|
protected_endpoints.insert(super::nut21::ProtectedEndpoint::new(
|
||||||
|
raw_endpoint.method,
|
||||||
|
path,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the final Settings struct
|
||||||
|
Ok(Settings {
|
||||||
|
bat_max_mint: raw.bat_max_mint,
|
||||||
|
protected_endpoints: protected_endpoints.into_iter().collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auth Token
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, 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) => cat.fmt(f),
|
||||||
|
Self::BlindAuth(bat) => bat.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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,
|
||||||
|
/// Auth Proof Dleq
|
||||||
|
pub dleq: ProofDleq,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthProof {
|
||||||
|
/// Y of AuthProof
|
||||||
|
pub fn y(&self) -> Result<PublicKey, Error> {
|
||||||
|
Ok(hash_to_curve(self.secret.as_bytes())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AuthProof> 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: Some(value.dleq),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Proof> for AuthProof {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: Proof) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
keyset_id: value.keyset_id,
|
||||||
|
secret: value.secret,
|
||||||
|
c: value.c,
|
||||||
|
dleq: value.dleq.ok_or({
|
||||||
|
tracing::warn!("Dleq proof not included in auth");
|
||||||
|
Error::DleqProofNotIncluded
|
||||||
|
})?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blind Auth Token
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct BlindAuthToken {
|
||||||
|
/// [AuthProof]
|
||||||
|
pub auth_proof: AuthProof,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlindAuthToken {
|
||||||
|
/// Create new [ `BlindAuthToken`]
|
||||||
|
pub fn new(auth_proof: AuthProof) -> Self {
|
||||||
|
BlindAuthToken { auth_proof }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self, Self::Err> {
|
||||||
|
// Check prefix and extract the base64 encoded part in one step
|
||||||
|
let encoded = s.strip_prefix("authA").ok_or(Error::InvalidPrefix)?;
|
||||||
|
|
||||||
|
// 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<BlindedMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MintAuthRequest {
|
||||||
|
/// Count of tokens
|
||||||
|
pub fn amount(&self) -> u64 {
|
||||||
|
self.outputs.len() as u64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use super::super::nut21::{Method, RoutePath};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_direct_paths() {
|
||||||
|
let json = r#"{
|
||||||
|
"bat_max_mint": 10,
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/v1/mint/bolt11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/swap"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let settings: Settings = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(settings.bat_max_mint, 10);
|
||||||
|
assert_eq!(settings.protected_endpoints.len(), 2);
|
||||||
|
|
||||||
|
// Check that both paths are included
|
||||||
|
let paths = settings
|
||||||
|
.protected_endpoints
|
||||||
|
.iter()
|
||||||
|
.map(|ep| (ep.method, ep.path))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert!(paths.contains(&(Method::Get, RoutePath::MintBolt11)));
|
||||||
|
assert!(paths.contains(&(Method::Post, RoutePath::Swap)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_with_regex() {
|
||||||
|
let json = r#"{
|
||||||
|
"bat_max_mint": 5,
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "^/v1/mint/.*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/v1/swap"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let settings: Settings = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(settings.bat_max_mint, 5);
|
||||||
|
assert_eq!(settings.protected_endpoints.len(), 3); // 2 mint paths + 1 swap path
|
||||||
|
|
||||||
|
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let deserialized_protected = settings.protected_endpoints.into_iter().collect();
|
||||||
|
|
||||||
|
assert_eq!(expected_protected, deserialized_protected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_invalid_regex() {
|
||||||
|
let json = r#"{
|
||||||
|
"bat_max_mint": 5,
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "(unclosed parenthesis"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let result = serde_json::from_str::<Settings>(json);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_settings_deserialize_all_paths() {
|
||||||
|
let json = r#"{
|
||||||
|
"bat_max_mint": 5,
|
||||||
|
"protected_endpoints": [
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": ".*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let settings: Settings = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
settings.protected_endpoints.len(),
|
||||||
|
RoutePath::iter().count()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,14 @@ pub mod nut18;
|
|||||||
pub mod nut19;
|
pub mod nut19;
|
||||||
pub mod nut20;
|
pub mod nut20;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use auth::{
|
||||||
|
nut21, nut22, AuthProof, AuthRequired, AuthToken, BlindAuthSettings, BlindAuthToken,
|
||||||
|
ClearAuthSettings, Method, MintAuthRequest, ProtectedEndpoint, RoutePath,
|
||||||
|
};
|
||||||
pub use nut00::{
|
pub use nut00::{
|
||||||
BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proof, Proofs, ProofsMethods,
|
BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proof, Proofs, ProofsMethods,
|
||||||
Token, TokenV3, TokenV4, Witness,
|
Token, TokenV3, TokenV4, Witness,
|
||||||
|
|||||||
@@ -445,6 +445,8 @@ pub enum CurrencyUnit {
|
|||||||
Usd,
|
Usd,
|
||||||
/// Euro
|
/// Euro
|
||||||
Eur,
|
Eur,
|
||||||
|
/// Auth
|
||||||
|
Auth,
|
||||||
/// Custom currency unit
|
/// Custom currency unit
|
||||||
Custom(String),
|
Custom(String),
|
||||||
}
|
}
|
||||||
@@ -458,6 +460,7 @@ impl CurrencyUnit {
|
|||||||
Self::Msat => Some(1),
|
Self::Msat => Some(1),
|
||||||
Self::Usd => Some(2),
|
Self::Usd => Some(2),
|
||||||
Self::Eur => Some(3),
|
Self::Eur => Some(3),
|
||||||
|
Self::Auth => Some(4),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,6 +475,7 @@ impl FromStr for CurrencyUnit {
|
|||||||
"MSAT" => Ok(Self::Msat),
|
"MSAT" => Ok(Self::Msat),
|
||||||
"USD" => Ok(Self::Usd),
|
"USD" => Ok(Self::Usd),
|
||||||
"EUR" => Ok(Self::Eur),
|
"EUR" => Ok(Self::Eur),
|
||||||
|
"AUTH" => Ok(Self::Auth),
|
||||||
c => Ok(Self::Custom(c.to_string())),
|
c => Ok(Self::Custom(c.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,6 +488,7 @@ impl fmt::Display for CurrencyUnit {
|
|||||||
CurrencyUnit::Msat => "MSAT",
|
CurrencyUnit::Msat => "MSAT",
|
||||||
CurrencyUnit::Usd => "USD",
|
CurrencyUnit::Usd => "USD",
|
||||||
CurrencyUnit::Eur => "EUR",
|
CurrencyUnit::Eur => "EUR",
|
||||||
|
CurrencyUnit::Auth => "AUTH",
|
||||||
CurrencyUnit::Custom(unit) => unit,
|
CurrencyUnit::Custom(unit) => unit,
|
||||||
};
|
};
|
||||||
if let Some(width) = f.width() {
|
if let Some(width) = f.width() {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use bitcoin::hashes::Hash;
|
|||||||
use bitcoin::key::Secp256k1;
|
use bitcoin::key::Secp256k1;
|
||||||
#[cfg(feature = "mint")]
|
#[cfg(feature = "mint")]
|
||||||
use bitcoin::secp256k1;
|
use bitcoin::secp256k1;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use serde_with::{serde_as, VecSkipError};
|
use serde_with::{serde_as, VecSkipError};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ pub enum Error {
|
|||||||
|
|
||||||
/// Keyset version
|
/// Keyset version
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
pub enum KeySetVersion {
|
pub enum KeySetVersion {
|
||||||
/// Current Version 00
|
/// Current Version 00
|
||||||
Version00,
|
Version00,
|
||||||
@@ -85,6 +86,7 @@ impl fmt::Display for KeySetVersion {
|
|||||||
/// which mint or keyset it was generated from.
|
/// which mint or keyset it was generated from.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
#[serde(into = "String", try_from = "String")]
|
#[serde(into = "String", try_from = "String")]
|
||||||
|
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
|
||||||
pub struct Id {
|
pub struct Id {
|
||||||
version: KeySetVersion,
|
version: KeySetVersion,
|
||||||
id: [u8; Self::BYTELEN],
|
id: [u8; Self::BYTELEN],
|
||||||
@@ -258,10 +260,22 @@ pub struct KeySetInfo {
|
|||||||
/// Mint will only sign from an active keyset
|
/// Mint will only sign from an active keyset
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
/// Input Fee PPK
|
/// Input Fee PPK
|
||||||
#[serde(default = "default_input_fee_ppk")]
|
#[serde(
|
||||||
|
deserialize_with = "deserialize_input_fee_ppk",
|
||||||
|
default = "default_input_fee_ppk"
|
||||||
|
)]
|
||||||
pub input_fee_ppk: u64,
|
pub input_fee_ppk: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
// This will either give us a u64 or null (which becomes None)
|
||||||
|
let opt = Option::<u64>::deserialize(deserializer)?;
|
||||||
|
Ok(opt.unwrap_or_else(default_input_fee_ppk))
|
||||||
|
}
|
||||||
|
|
||||||
fn default_input_fee_ppk() -> u64 {
|
fn default_input_fee_ppk() -> u64 {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -484,6 +498,10 @@ mod test {
|
|||||||
let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#;
|
let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#;
|
||||||
|
|
||||||
let _keyset_response: KeySetInfo = serde_json::from_str(h).unwrap();
|
let _keyset_response: KeySetInfo = serde_json::from_str(h).unwrap();
|
||||||
|
|
||||||
|
let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true, "input_fee_ppk":null}"#;
|
||||||
|
|
||||||
|
let _keyset_response: KeySetInfo = serde_json::from_str(h).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
//!
|
//!
|
||||||
//! <https://github.com/cashubtc/nuts/blob/main/06.md>
|
//! <https://github.com/cashubtc/nuts/blob/main/06.md>
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
use super::nut01::PublicKey;
|
use super::nut01::PublicKey;
|
||||||
use super::nut17::SupportedMethods;
|
use super::nut17::SupportedMethods;
|
||||||
use super::nut19::CachedEndpoint;
|
use super::nut19::CachedEndpoint;
|
||||||
use super::{nut04, nut05, nut15, nut19, MppMethodSettings};
|
use super::{nut04, nut05, nut15, nut19, MppMethodSettings};
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use super::{AuthRequired, BlindAuthSettings, ClearAuthSettings, ProtectedEndpoint};
|
||||||
|
|
||||||
/// Mint Version
|
/// Mint Version
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
@@ -211,6 +216,46 @@ impl MintInfo {
|
|||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get protected endpoints
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub fn protected_endpoints(&self) -> HashMap<ProtectedEndpoint, AuthRequired> {
|
||||||
|
let mut protected_endpoints = HashMap::new();
|
||||||
|
|
||||||
|
if let Some(nut21_settings) = &self.nuts.nut21 {
|
||||||
|
for endpoint in nut21_settings.protected_endpoints.iter() {
|
||||||
|
protected_endpoints.insert(*endpoint, AuthRequired::Clear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(nut22_settings) = &self.nuts.nut22 {
|
||||||
|
for endpoint in nut22_settings.protected_endpoints.iter() {
|
||||||
|
protected_endpoints.insert(*endpoint, AuthRequired::Blind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected_endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Openid discovery of the mint if it is set
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub fn openid_discovery(&self) -> Option<String> {
|
||||||
|
self.nuts
|
||||||
|
.nut21
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.openid_discovery.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Openid discovery of the mint if it is set
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub fn client_id(&self) -> Option<String> {
|
||||||
|
self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Max bat mint
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub fn bat_max_mint(&self) -> Option<u64> {
|
||||||
|
self.nuts.nut22.as_ref().map(|s| s.bat_max_mint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supported nuts and settings
|
/// Supported nuts and settings
|
||||||
@@ -269,6 +314,16 @@ pub struct Nuts {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(rename = "20")]
|
#[serde(rename = "20")]
|
||||||
pub nut20: SupportedSettings,
|
pub nut20: SupportedSettings,
|
||||||
|
/// NUT21 Settings
|
||||||
|
#[serde(rename = "21")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub nut21: Option<ClearAuthSettings>,
|
||||||
|
/// NUT22 Settings
|
||||||
|
#[serde(rename = "22")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub nut22: Option<BlindAuthSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Nuts {
|
impl Nuts {
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ repository = "https://github.com/cashubtc/cdk.git"
|
|||||||
rust-version = "1.75.0" # MSRV
|
rust-version = "1.75.0" # MSRV
|
||||||
description = "Cashu CDK axum webserver"
|
description = "Cashu CDK axum webserver"
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
default = ["auth"]
|
||||||
redis = ["dep:redis"]
|
redis = ["dep:redis"]
|
||||||
swagger = ["cdk/swagger", "dep:utoipa"]
|
swagger = ["cdk/swagger", "dep:utoipa"]
|
||||||
|
auth = ["cdk/auth"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
@@ -18,7 +21,7 @@ async-trait.workspace = true
|
|||||||
axum = { workspace = true, features = ["ws"] }
|
axum = { workspace = true, features = ["ws"] }
|
||||||
cdk = { workspace = true, features = [
|
cdk = { workspace = true, features = [
|
||||||
"mint",
|
"mint",
|
||||||
] }
|
]}
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
utoipa = { workspace = true, optional = true }
|
utoipa = { workspace = true, optional = true }
|
||||||
|
|||||||
194
crates/cdk-axum/src/auth.rs
Normal file
194
crates/cdk-axum/src/auth.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
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::{Json, Router};
|
||||||
|
#[cfg(feature = "swagger")]
|
||||||
|
use cdk::error::ErrorResponse;
|
||||||
|
use cdk::nuts::{
|
||||||
|
AuthToken, BlindAuthToken, KeysResponse, KeysetResponse, MintAuthRequest, MintBolt11Response,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
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, Serialize, Deserialize)]
|
||||||
|
pub enum AuthHeader {
|
||||||
|
/// Clear Auth token
|
||||||
|
Clear(String),
|
||||||
|
/// Blind Auth token
|
||||||
|
Blind(BlindAuthToken),
|
||||||
|
/// No auth
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AuthHeader> for Option<AuthToken> {
|
||||||
|
fn from(value: AuthHeader) -> Option<AuthToken> {
|
||||||
|
match value {
|
||||||
|
AuthHeader::Clear(token) => Some(AuthToken::ClearAuth(token)),
|
||||||
|
AuthHeader::Blind(token) => Some(AuthToken::BlindAuth(token)),
|
||||||
|
AuthHeader::None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> FromRequestParts<S> for AuthHeader
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = (StatusCode, String);
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
// 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.
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub async fn get_auth_keysets(
|
||||||
|
State(state): State<MintState>,
|
||||||
|
) -> Result<Json<KeysetResponse>, 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<MintState>,
|
||||||
|
) -> Result<Json<KeysResponse>, 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<MintState>,
|
||||||
|
Json(payload): Json<MintAuthRequest>,
|
||||||
|
) -> Result<Json<MintBolt11Response>, Response> {
|
||||||
|
let auth_token = match auth {
|
||||||
|
AuthHeader::Clear(cat) => {
|
||||||
|
if cat.is_empty() {
|
||||||
|
tracing::debug!("Received blind auth mint request without cat");
|
||||||
|
return Err(into_response(cdk::Error::ClearAuthRequired));
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthToken::ClearAuth(cat)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::debug!("Received blind auth mint request without cat");
|
||||||
|
return Err(into_response(cdk::Error::ClearAuthRequired));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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<MintState> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -6,12 +6,16 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use auth::create_auth_router;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use cache::HttpCache;
|
use cache::HttpCache;
|
||||||
use cdk::mint::Mint;
|
use cdk::mint::Mint;
|
||||||
use router_handlers::*;
|
use router_handlers::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
mod router_handlers;
|
mod router_handlers;
|
||||||
mod ws;
|
mod ws;
|
||||||
@@ -165,7 +169,15 @@ pub async fn create_mint_router_with_custom_cache(
|
|||||||
.route("/info", get(get_mint_info))
|
.route("/info", get(get_mint_info))
|
||||||
.route("/restore", post(post_restore));
|
.route("/restore", post(post_restore));
|
||||||
|
|
||||||
let mint_router = Router::new().nest("/v1", v1_router).with_state(state);
|
let mint_router = Router::new().nest("/v1", v1_router);
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let mint_router = {
|
||||||
|
let auth_router = create_auth_router(state.clone());
|
||||||
|
mint_router.nest("/v1", auth_router)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mint_router = mint_router.with_state(state);
|
||||||
|
|
||||||
Ok(mint_router)
|
Ok(mint_router)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ use axum::extract::{Json, Path, State};
|
|||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use cdk::error::ErrorResponse;
|
use cdk::error::ErrorResponse;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
|
||||||
use cdk::nuts::{
|
use cdk::nuts::{
|
||||||
CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request,
|
CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request,
|
||||||
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
|
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
|
||||||
@@ -15,6 +17,8 @@ use paste::paste;
|
|||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::auth::AuthHeader;
|
||||||
use crate::ws::main_websocket;
|
use crate::ws::main_websocket;
|
||||||
use crate::MintState;
|
use crate::MintState;
|
||||||
|
|
||||||
@@ -24,25 +28,29 @@ macro_rules! post_cache_wrapper {
|
|||||||
/// Cache wrapper function for $handler:
|
/// Cache wrapper function for $handler:
|
||||||
/// Wrap $handler into a function that caches responses using the request as key
|
/// Wrap $handler into a function that caches responses using the request as key
|
||||||
pub async fn [<cache_ $handler>](
|
pub async fn [<cache_ $handler>](
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
state: State<MintState>,
|
state: State<MintState>,
|
||||||
payload: Json<$request_type>
|
payload: Json<$request_type>
|
||||||
) -> Result<Json<$response_type>, Response> {
|
) -> Result<Json<$response_type>, Response> {
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
let json_extracted_payload = payload.deref();
|
let json_extracted_payload = payload.deref();
|
||||||
let State(mint_state) = state.clone();
|
let State(mint_state) = state.clone();
|
||||||
let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
|
let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
|
||||||
Some(key) => key,
|
Some(key) => key,
|
||||||
None => {
|
None => {
|
||||||
// Could not calculate key, just return the handler result
|
// Could not calculate key, just return the handler result
|
||||||
return $handler(state, payload).await;
|
#[cfg(feature = "auth")]
|
||||||
|
return $handler(auth, state, payload).await;
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
return $handler( state, payload).await;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
|
if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
|
||||||
return Ok(Json(cached_response));
|
return Ok(Json(cached_response));
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let response = $handler(auth, state, payload).await?;
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
let response = $handler(state, payload).await?;
|
let response = $handler(state, payload).await?;
|
||||||
mint_state.cache.set(cache_key, &response.deref()).await;
|
mint_state.cache.set(cache_key, &response.deref()).await;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
@@ -74,7 +82,10 @@ post_cache_wrapper!(
|
|||||||
/// Get the public keys of the newest mint keyset
|
/// Get the public keys of the newest mint keyset
|
||||||
///
|
///
|
||||||
/// This endpoint returns a dictionary of all supported token values of the mint and their associated public key.
|
/// This endpoint returns a dictionary of all supported token values of the mint and their associated public key.
|
||||||
pub async fn get_keys(State(state): State<MintState>) -> Result<Json<KeysResponse>, Response> {
|
#[instrument(skip_all)]
|
||||||
|
pub(crate) async fn get_keys(
|
||||||
|
State(state): State<MintState>,
|
||||||
|
) -> Result<Json<KeysResponse>, Response> {
|
||||||
let pubkeys = state.mint.pubkeys().await.map_err(|err| {
|
let pubkeys = state.mint.pubkeys().await.map_err(|err| {
|
||||||
tracing::error!("Could not get keys: {}", err);
|
tracing::error!("Could not get keys: {}", err);
|
||||||
into_response(err)
|
into_response(err)
|
||||||
@@ -98,7 +109,8 @@ pub async fn get_keys(State(state): State<MintState>) -> Result<Json<KeysRespons
|
|||||||
/// Get the public keys of a specific keyset
|
/// Get the public keys of a specific keyset
|
||||||
///
|
///
|
||||||
/// Get the public keys of the mint from a specific keyset ID.
|
/// Get the public keys of the mint from a specific keyset ID.
|
||||||
pub async fn get_keyset_pubkeys(
|
#[instrument(skip_all, fields(keyset_id = ?keyset_id))]
|
||||||
|
pub(crate) async fn get_keyset_pubkeys(
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Path(keyset_id): Path<Id>,
|
Path(keyset_id): Path<Id>,
|
||||||
) -> Result<Json<KeysResponse>, Response> {
|
) -> Result<Json<KeysResponse>, Response> {
|
||||||
@@ -122,7 +134,10 @@ pub async fn get_keyset_pubkeys(
|
|||||||
/// Get all active keyset IDs of the mint
|
/// 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.
|
/// This endpoint returns a list of keysets that the mint currently supports and will accept tokens from.
|
||||||
pub async fn get_keysets(State(state): State<MintState>) -> Result<Json<KeysetResponse>, Response> {
|
#[instrument(skip_all)]
|
||||||
|
pub(crate) async fn get_keysets(
|
||||||
|
State(state): State<MintState>,
|
||||||
|
) -> Result<Json<KeysetResponse>, Response> {
|
||||||
let keysets = state.mint.keysets().await.map_err(|err| {
|
let keysets = state.mint.keysets().await.map_err(|err| {
|
||||||
tracing::error!("Could not get keysets: {}", err);
|
tracing::error!("Could not get keysets: {}", err);
|
||||||
into_response(err)
|
into_response(err)
|
||||||
@@ -144,10 +159,22 @@ pub async fn get_keysets(State(state): State<MintState>) -> Result<Json<KeysetRe
|
|||||||
/// Request a quote for minting of new tokens
|
/// Request a quote for minting of new tokens
|
||||||
///
|
///
|
||||||
/// Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow.
|
/// Request minting of new tokens. The mint responds with a Lightning invoice. This endpoint can be used for a Lightning invoice UX flow.
|
||||||
pub async fn post_mint_bolt11_quote(
|
#[instrument(skip_all, fields(amount = ?payload.amount))]
|
||||||
|
pub(crate) async fn post_mint_bolt11_quote(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Json(payload): Json<MintQuoteBolt11Request>,
|
Json(payload): Json<MintQuoteBolt11Request>,
|
||||||
) -> Result<Json<MintQuoteBolt11Response<Uuid>>, Response> {
|
) -> Result<Json<MintQuoteBolt11Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
|
||||||
let quote = state
|
let quote = state
|
||||||
.mint
|
.mint
|
||||||
.get_mint_bolt11_quote(payload)
|
.get_mint_bolt11_quote(payload)
|
||||||
@@ -172,10 +199,24 @@ pub async fn post_mint_bolt11_quote(
|
|||||||
/// Get mint quote by ID
|
/// Get mint quote by ID
|
||||||
///
|
///
|
||||||
/// Get mint quote state.
|
/// Get mint quote state.
|
||||||
pub async fn get_check_mint_bolt11_quote(
|
#[instrument(skip_all, fields(quote_id = ?quote_id))]
|
||||||
|
pub(crate) async fn get_check_mint_bolt11_quote(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Path(quote_id): Path<Uuid>,
|
Path(quote_id): Path<Uuid>,
|
||||||
) -> Result<Json<MintQuoteBolt11Response<Uuid>>, Response> {
|
) -> Result<Json<MintQuoteBolt11Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let quote = state
|
let quote = state
|
||||||
.mint
|
.mint
|
||||||
.check_mint_quote("e_id)
|
.check_mint_quote("e_id)
|
||||||
@@ -188,7 +229,11 @@ pub async fn get_check_mint_bolt11_quote(
|
|||||||
Ok(Json(quote))
|
Ok(Json(quote))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ws_handler(State(state): State<MintState>, ws: WebSocketUpgrade) -> impl IntoResponse {
|
#[instrument(skip_all)]
|
||||||
|
pub(crate) async fn ws_handler(
|
||||||
|
State(state): State<MintState>,
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
) -> impl IntoResponse {
|
||||||
ws.on_upgrade(|ws| main_websocket(ws, state))
|
ws.on_upgrade(|ws| main_websocket(ws, state))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,10 +252,24 @@ pub async fn ws_handler(State(state): State<MintState>, ws: WebSocketUpgrade) ->
|
|||||||
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
|
(status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
|
||||||
)
|
)
|
||||||
))]
|
))]
|
||||||
pub async fn post_mint_bolt11(
|
#[instrument(skip_all, fields(quote_id = ?payload.quote))]
|
||||||
|
pub(crate) async fn post_mint_bolt11(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Json(payload): Json<MintBolt11Request<Uuid>>,
|
Json(payload): Json<MintBolt11Request<Uuid>>,
|
||||||
) -> Result<Json<MintBolt11Response>, Response> {
|
) -> Result<Json<MintBolt11Response>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let res = state
|
let res = state
|
||||||
.mint
|
.mint
|
||||||
.process_mint_request(payload)
|
.process_mint_request(payload)
|
||||||
@@ -235,10 +294,23 @@ pub async fn post_mint_bolt11(
|
|||||||
))]
|
))]
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
/// Request a quote for melting tokens
|
/// Request a quote for melting tokens
|
||||||
pub async fn post_melt_bolt11_quote(
|
pub(crate) async fn post_melt_bolt11_quote(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Json(payload): Json<MeltQuoteBolt11Request>,
|
Json(payload): Json<MeltQuoteBolt11Request>,
|
||||||
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
|
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let quote = state
|
let quote = state
|
||||||
.mint
|
.mint
|
||||||
.get_melt_bolt11_quote(&payload)
|
.get_melt_bolt11_quote(&payload)
|
||||||
@@ -263,11 +335,24 @@ pub async fn post_melt_bolt11_quote(
|
|||||||
/// Get melt quote by ID
|
/// Get melt quote by ID
|
||||||
///
|
///
|
||||||
/// Get melt quote state.
|
/// Get melt quote state.
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all, fields(quote_id = ?quote_id))]
|
||||||
pub async fn get_check_melt_bolt11_quote(
|
pub(crate) async fn get_check_melt_bolt11_quote(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Path(quote_id): Path<Uuid>,
|
Path(quote_id): Path<Uuid>,
|
||||||
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
|
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let quote = state
|
let quote = state
|
||||||
.mint
|
.mint
|
||||||
.check_melt_quote("e_id)
|
.check_melt_quote("e_id)
|
||||||
@@ -294,10 +379,23 @@ pub async fn get_check_melt_bolt11_quote(
|
|||||||
///
|
///
|
||||||
/// Requests tokens to be destroyed and sent out via Lightning.
|
/// Requests tokens to be destroyed and sent out via Lightning.
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn post_melt_bolt11(
|
pub(crate) async fn post_melt_bolt11(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Json(payload): Json<MeltBolt11Request<Uuid>>,
|
Json(payload): Json<MeltBolt11Request<Uuid>>,
|
||||||
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
|
) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let res = state
|
let res = state
|
||||||
.mint
|
.mint
|
||||||
.melt_bolt11(&payload)
|
.melt_bolt11(&payload)
|
||||||
@@ -320,10 +418,24 @@ pub async fn post_melt_bolt11(
|
|||||||
/// Check whether a proof is spent already or is pending in a transaction
|
/// Check whether a proof is spent already or is pending in a transaction
|
||||||
///
|
///
|
||||||
/// Check whether a secret has been spent already or not.
|
/// Check whether a secret has been spent already or not.
|
||||||
pub async fn post_check(
|
#[instrument(skip_all, fields(y_count = ?payload.ys.len()))]
|
||||||
|
pub(crate) async fn post_check(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Json(payload): Json<CheckStateRequest>,
|
Json(payload): Json<CheckStateRequest>,
|
||||||
) -> Result<Json<CheckStateResponse>, Response> {
|
) -> Result<Json<CheckStateResponse>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let state = state.mint.check_state(&payload).await.map_err(|err| {
|
let state = state.mint.check_state(&payload).await.map_err(|err| {
|
||||||
tracing::error!("Could not check state of proofs");
|
tracing::error!("Could not check state of proofs");
|
||||||
into_response(err)
|
into_response(err)
|
||||||
@@ -341,7 +453,10 @@ pub async fn post_check(
|
|||||||
)
|
)
|
||||||
))]
|
))]
|
||||||
/// Mint information, operator contact information, and other info
|
/// Mint information, operator contact information, and other info
|
||||||
pub async fn get_mint_info(State(state): State<MintState>) -> Result<Json<MintInfo>, Response> {
|
#[instrument(skip_all)]
|
||||||
|
pub(crate) async fn get_mint_info(
|
||||||
|
State(state): State<MintState>,
|
||||||
|
) -> Result<Json<MintInfo>, Response> {
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
state
|
state
|
||||||
.mint
|
.mint
|
||||||
@@ -371,10 +486,24 @@ pub async fn get_mint_info(State(state): State<MintState>) -> Result<Json<MintIn
|
|||||||
/// Requests a set of Proofs to be swapped for another set of BlindSignatures.
|
/// Requests a set of Proofs to be swapped for another set of BlindSignatures.
|
||||||
///
|
///
|
||||||
/// This endpoint can be used by Alice to swap a set of proofs before making a payment to Carol. It can then used by Carol to redeem the tokens for new proofs.
|
/// This endpoint can be used by Alice to swap a set of proofs before making a payment to Carol. It can then used by Carol to redeem the tokens for new proofs.
|
||||||
pub async fn post_swap(
|
#[instrument(skip_all, fields(inputs_count = ?payload.inputs.len()))]
|
||||||
|
pub(crate) async fn post_swap(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Json(payload): Json<SwapRequest>,
|
Json(payload): Json<SwapRequest>,
|
||||||
) -> Result<Json<SwapResponse>, Response> {
|
) -> Result<Json<SwapResponse>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let swap_response = state
|
let swap_response = state
|
||||||
.mint
|
.mint
|
||||||
.process_swap_request(payload)
|
.process_swap_request(payload)
|
||||||
@@ -383,6 +512,7 @@ pub async fn post_swap(
|
|||||||
tracing::error!("Could not process swap request: {}", err);
|
tracing::error!("Could not process swap request: {}", err);
|
||||||
into_response(err)
|
into_response(err)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Json(swap_response))
|
Ok(Json(swap_response))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,10 +527,24 @@ pub async fn post_swap(
|
|||||||
)
|
)
|
||||||
))]
|
))]
|
||||||
/// Restores blind signature for a set of outputs.
|
/// Restores blind signature for a set of outputs.
|
||||||
pub async fn post_restore(
|
#[instrument(skip_all, fields(outputs_count = ?payload.outputs.len()))]
|
||||||
|
pub(crate) async fn post_restore(
|
||||||
|
#[cfg(feature = "auth")] auth: AuthHeader,
|
||||||
State(state): State<MintState>,
|
State(state): State<MintState>,
|
||||||
Json(payload): Json<RestoreRequest>,
|
Json(payload): Json<RestoreRequest>,
|
||||||
) -> Result<Json<RestoreResponse>, Response> {
|
) -> Result<Json<RestoreResponse>, Response> {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.mint
|
||||||
|
.verify_auth(
|
||||||
|
auth.into(),
|
||||||
|
&ProtectedEndpoint::new(Method::Post, RoutePath::Restore),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(into_response)?;
|
||||||
|
}
|
||||||
|
|
||||||
let restore_response = state.mint.restore(payload).await.map_err(|err| {
|
let restore_response = state.mint.restore(payload).await.map_err(|err| {
|
||||||
tracing::error!("Could not process restore: {}", err);
|
tracing::error!("Could not process restore: {}", err);
|
||||||
into_response(err)
|
into_response(err)
|
||||||
@@ -409,7 +553,8 @@ pub async fn post_restore(
|
|||||||
Ok(Json(restore_response))
|
Ok(Json(restore_response))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn into_response<T>(error: T) -> Response
|
#[instrument(skip_all)]
|
||||||
|
pub(crate) fn into_response<T>(error: T) -> Response
|
||||||
where
|
where
|
||||||
T: Into<ErrorResponse>,
|
T: Into<ErrorResponse>,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ sqlcipher = ["cdk-sqlite/sqlcipher"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
bip39.workspace = true
|
bip39.workspace = true
|
||||||
cdk = { workspace = true, default-features = false, features = ["wallet"]}
|
cdk = { workspace = true, default-features = false, features = ["wallet", "auth"]}
|
||||||
cdk-redb = { workspace = true, features = ["wallet"] }
|
cdk-redb = { workspace = true, features = ["wallet"] }
|
||||||
cdk-sqlite = { workspace = true, features = ["wallet"] }
|
cdk-sqlite = { workspace = true, features = ["wallet"] }
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ use bip39::rand::{thread_rng, Rng};
|
|||||||
use bip39::Mnemonic;
|
use bip39::Mnemonic;
|
||||||
use cdk::cdk_database;
|
use cdk::cdk_database;
|
||||||
use cdk::cdk_database::WalletDatabase;
|
use cdk::cdk_database::WalletDatabase;
|
||||||
use cdk::wallet::client::HttpClient;
|
use cdk::wallet::{HttpClient, MultiMintWallet, Wallet, WalletBuilder};
|
||||||
use cdk::wallet::{MultiMintWallet, Wallet};
|
|
||||||
use cdk_redb::WalletRedbDatabase;
|
use cdk_redb::WalletRedbDatabase;
|
||||||
use cdk_sqlite::WalletSqliteDatabase;
|
use cdk_sqlite::WalletSqliteDatabase;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
@@ -19,6 +18,7 @@ use url::Url;
|
|||||||
|
|
||||||
mod nostr_storage;
|
mod nostr_storage;
|
||||||
mod sub_commands;
|
mod sub_commands;
|
||||||
|
mod token_storage;
|
||||||
|
|
||||||
const DEFAULT_WORK_DIR: &str = ".cdk-cli";
|
const DEFAULT_WORK_DIR: &str = ".cdk-cli";
|
||||||
|
|
||||||
@@ -83,6 +83,12 @@ enum Commands {
|
|||||||
PayRequest(sub_commands::pay_request::PayRequestSubCommand),
|
PayRequest(sub_commands::pay_request::PayRequestSubCommand),
|
||||||
/// Create Payment request
|
/// Create Payment request
|
||||||
CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
|
CreateRequest(sub_commands::create_request::CreateRequestSubCommand),
|
||||||
|
/// Mint blind auth proofs
|
||||||
|
MintBlindAuth(sub_commands::mint_blind_auth::MintBlindAuthSubCommand),
|
||||||
|
/// Cat login with username/password
|
||||||
|
CatLogin(sub_commands::cat_login::CatLoginSubCommand),
|
||||||
|
/// Cat login with device code flow
|
||||||
|
CatDeviceLogin(sub_commands::cat_device_login::CatDeviceLoginSubCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -158,18 +164,19 @@ async fn main() -> Result<()> {
|
|||||||
let mints = localstore.get_mints().await?;
|
let mints = localstore.get_mints().await?;
|
||||||
|
|
||||||
for (mint_url, _) in mints {
|
for (mint_url, _) in mints {
|
||||||
let mut wallet = Wallet::new(
|
let mut builder = WalletBuilder::new()
|
||||||
&mint_url.to_string(),
|
.mint_url(mint_url.clone())
|
||||||
cdk::nuts::CurrencyUnit::Sat,
|
.unit(cdk::nuts::CurrencyUnit::Sat)
|
||||||
localstore.clone(),
|
.localstore(localstore.clone())
|
||||||
&mnemonic.to_seed_normalized(""),
|
.seed(&mnemonic.to_seed_normalized(""));
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
if let Some(proxy_url) = args.proxy.as_ref() {
|
if let Some(proxy_url) = args.proxy.as_ref() {
|
||||||
let http_client = HttpClient::with_proxy(mint_url, proxy_url.clone(), None, true)?;
|
let http_client = HttpClient::with_proxy(mint_url, proxy_url.clone(), None, true)?;
|
||||||
wallet.set_client(http_client);
|
builder = builder.client(http_client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let wallet = builder.build()?;
|
||||||
|
|
||||||
wallets.push(wallet);
|
wallets.push(wallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,5 +249,35 @@ async fn main() -> Result<()> {
|
|||||||
Commands::CreateRequest(sub_command_args) => {
|
Commands::CreateRequest(sub_command_args) => {
|
||||||
sub_commands::create_request::create_request(&multi_mint_wallet, sub_command_args).await
|
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,
|
||||||
|
&work_dir,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Commands::CatLogin(sub_command_args) => {
|
||||||
|
sub_commands::cat_login::cat_login(
|
||||||
|
&multi_mint_wallet,
|
||||||
|
&mnemonic.to_seed_normalized(""),
|
||||||
|
localstore,
|
||||||
|
sub_command_args,
|
||||||
|
&work_dir,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Commands::CatDeviceLogin(sub_command_args) => {
|
||||||
|
sub_commands::cat_device_login::cat_device_login(
|
||||||
|
&multi_mint_wallet,
|
||||||
|
&mnemonic.to_seed_normalized(""),
|
||||||
|
localstore,
|
||||||
|
sub_command_args,
|
||||||
|
&work_dir,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
196
crates/cdk-cli/src/sub_commands/cat_device_login.rs
Normal file
196
crates/cdk-cli/src/sub_commands/cat_device_login.rs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use cdk::cdk_database::{Error, WalletDatabase};
|
||||||
|
use cdk::mint_url::MintUrl;
|
||||||
|
use cdk::nuts::{CurrencyUnit, MintInfo};
|
||||||
|
use cdk::wallet::types::WalletKey;
|
||||||
|
use cdk::wallet::{MultiMintWallet, Wallet};
|
||||||
|
use cdk::OidcClient;
|
||||||
|
use clap::Args;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
use crate::token_storage;
|
||||||
|
|
||||||
|
#[derive(Args, Serialize, Deserialize)]
|
||||||
|
pub struct CatDeviceLoginSubCommand {
|
||||||
|
/// Mint url
|
||||||
|
mint_url: MintUrl,
|
||||||
|
/// Currency unit e.g. sat
|
||||||
|
#[arg(default_value = "sat")]
|
||||||
|
#[arg(short, long)]
|
||||||
|
unit: String,
|
||||||
|
/// Client ID for OIDC authentication
|
||||||
|
#[arg(default_value = "cashu-client")]
|
||||||
|
#[arg(long)]
|
||||||
|
client_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cat_device_login(
|
||||||
|
multi_mint_wallet: &MultiMintWallet,
|
||||||
|
seed: &[u8],
|
||||||
|
localstore: Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
|
||||||
|
sub_command_args: &CatDeviceLoginSubCommand,
|
||||||
|
work_dir: &Path,
|
||||||
|
) -> 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)?;
|
||||||
|
|
||||||
|
multi_mint_wallet.add_wallet(wallet.clone()).await;
|
||||||
|
wallet
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mint_info = wallet.get_mint_info().await?.expect("Mint info not found");
|
||||||
|
|
||||||
|
let (access_token, refresh_token) =
|
||||||
|
get_device_code_token(&mint_info, &sub_command_args.client_id).await;
|
||||||
|
|
||||||
|
// Save tokens to file in work directory
|
||||||
|
if let Err(e) =
|
||||||
|
token_storage::save_tokens(work_dir, &mint_url, &access_token, &refresh_token).await
|
||||||
|
{
|
||||||
|
println!("Warning: Failed to save tokens to file: {}", e);
|
||||||
|
} else {
|
||||||
|
println!("Tokens saved to work directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print a cute ASCII cat
|
||||||
|
println!("\nAuthentication successful! 🎉\n");
|
||||||
|
println!("\nYour tokens:");
|
||||||
|
println!("access_token: {}", access_token);
|
||||||
|
println!("refresh_token: {}", refresh_token);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_device_code_token(mint_info: &MintInfo, client_id: &str) -> (String, String) {
|
||||||
|
let openid_discovery = mint_info
|
||||||
|
.nuts
|
||||||
|
.nut21
|
||||||
|
.clone()
|
||||||
|
.expect("Nut21 defined")
|
||||||
|
.openid_discovery;
|
||||||
|
|
||||||
|
let oidc_client = OidcClient::new(openid_discovery);
|
||||||
|
|
||||||
|
// Get the OIDC configuration
|
||||||
|
let oidc_config = oidc_client
|
||||||
|
.get_oidc_config()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get OIDC config");
|
||||||
|
|
||||||
|
// Get the device authorization endpoint
|
||||||
|
let device_auth_url = oidc_config.device_authorization_endpoint;
|
||||||
|
|
||||||
|
// Make the device code request
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let device_code_response = client
|
||||||
|
.post(device_auth_url)
|
||||||
|
.form(&[("client_id", client_id)])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send device code request");
|
||||||
|
|
||||||
|
let device_code_data: serde_json::Value = device_code_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse device code response");
|
||||||
|
|
||||||
|
let device_code = device_code_data["device_code"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No device code in response");
|
||||||
|
|
||||||
|
let user_code = device_code_data["user_code"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No user code in response");
|
||||||
|
|
||||||
|
let verification_uri = device_code_data["verification_uri"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No verification URI in response");
|
||||||
|
|
||||||
|
let verification_uri_complete = device_code_data["verification_uri_complete"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or(verification_uri);
|
||||||
|
|
||||||
|
let interval = device_code_data["interval"].as_u64().unwrap_or(5);
|
||||||
|
|
||||||
|
println!("\nTo login, visit: {}", verification_uri);
|
||||||
|
println!("And enter code: {}\n", user_code);
|
||||||
|
|
||||||
|
if verification_uri_complete != verification_uri {
|
||||||
|
println!(
|
||||||
|
"Or visit this URL directly: {}\n",
|
||||||
|
verification_uri_complete
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll for the token
|
||||||
|
let token_url = oidc_config.token_endpoint;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_secs(interval)).await;
|
||||||
|
|
||||||
|
let token_response = client
|
||||||
|
.post(&token_url)
|
||||||
|
.form(&[
|
||||||
|
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||||
|
("device_code", device_code),
|
||||||
|
("client_id", client_id),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send token request");
|
||||||
|
|
||||||
|
if token_response.status().is_success() {
|
||||||
|
let token_data: serde_json::Value = token_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse token response");
|
||||||
|
|
||||||
|
let access_token = token_data["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No access token in response")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let refresh_token = token_data["refresh_token"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No refresh token in response")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
return (access_token, refresh_token);
|
||||||
|
} else {
|
||||||
|
let error_data: serde_json::Value = token_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse error response");
|
||||||
|
|
||||||
|
let error = error_data["error"].as_str().unwrap_or("unknown_error");
|
||||||
|
|
||||||
|
// If the user hasn't completed the flow yet, continue polling
|
||||||
|
if error == "authorization_pending" || error == "slow_down" {
|
||||||
|
if error == "slow_down" {
|
||||||
|
// If we're polling too fast, slow down
|
||||||
|
sleep(Duration::from_secs(interval + 5)).await;
|
||||||
|
}
|
||||||
|
println!("Waiting for user to complete authentication...");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// For other errors, exit with an error message
|
||||||
|
panic!("Authentication failed: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
140
crates/cdk-cli/src/sub_commands/cat_login.rs
Normal file
140
crates/cdk-cli/src/sub_commands/cat_login.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
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, MintInfo};
|
||||||
|
use cdk::wallet::types::WalletKey;
|
||||||
|
use cdk::wallet::{MultiMintWallet, Wallet};
|
||||||
|
use cdk::OidcClient;
|
||||||
|
use clap::Args;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::token_storage;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
/// Client ID for OIDC authentication
|
||||||
|
#[arg(default_value = "cashu-client")]
|
||||||
|
#[arg(long)]
|
||||||
|
client_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cat_login(
|
||||||
|
multi_mint_wallet: &MultiMintWallet,
|
||||||
|
seed: &[u8],
|
||||||
|
localstore: Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
|
||||||
|
sub_command_args: &CatLoginSubCommand,
|
||||||
|
work_dir: &Path,
|
||||||
|
) -> 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)?;
|
||||||
|
|
||||||
|
multi_mint_wallet.add_wallet(wallet.clone()).await;
|
||||||
|
wallet
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mint_info = wallet.get_mint_info().await?.expect("Mint info not found");
|
||||||
|
|
||||||
|
let (access_token, refresh_token) = get_access_token(
|
||||||
|
&mint_info,
|
||||||
|
&sub_command_args.client_id,
|
||||||
|
&sub_command_args.username,
|
||||||
|
&sub_command_args.password,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Save tokens to file in work directory
|
||||||
|
if let Err(e) =
|
||||||
|
token_storage::save_tokens(work_dir, &mint_url, &access_token, &refresh_token).await
|
||||||
|
{
|
||||||
|
println!("Warning: Failed to save tokens to file: {}", e);
|
||||||
|
} else {
|
||||||
|
println!("Tokens saved to work directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nAuthentication successful! 🎉\n");
|
||||||
|
println!("\nYour tokens:");
|
||||||
|
println!("access_token: {}", access_token);
|
||||||
|
println!("refresh_token: {}", refresh_token);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_access_token(
|
||||||
|
mint_info: &MintInfo,
|
||||||
|
client_id: &str,
|
||||||
|
user: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> (String, String) {
|
||||||
|
let openid_discovery = mint_info
|
||||||
|
.nuts
|
||||||
|
.nut21
|
||||||
|
.clone()
|
||||||
|
.expect("Nut21 defined")
|
||||||
|
.openid_discovery;
|
||||||
|
|
||||||
|
let oidc_client = OidcClient::new(openid_discovery);
|
||||||
|
|
||||||
|
// Get the token endpoint from the OIDC configuration
|
||||||
|
let token_url = oidc_client
|
||||||
|
.get_oidc_config()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get OIDC config")
|
||||||
|
.token_endpoint;
|
||||||
|
|
||||||
|
// Create the request parameters
|
||||||
|
let params = [
|
||||||
|
("grant_type", "password"),
|
||||||
|
("client_id", client_id),
|
||||||
|
("username", user),
|
||||||
|
("password", password),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make the token request directly
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.post(token_url)
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send token request");
|
||||||
|
|
||||||
|
let token_response: serde_json::Value = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse token response");
|
||||||
|
|
||||||
|
let access_token = token_response["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No access token in response")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let refresh_token = token_response["refresh_token"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No refresh token in response")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
(access_token, refresh_token)
|
||||||
|
}
|
||||||
@@ -53,6 +53,8 @@ pub async fn mint(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
wallet.get_mint_info().await?;
|
||||||
|
|
||||||
let quote_id = match &sub_command_args.quote_id {
|
let quote_id = match &sub_command_args.quote_id {
|
||||||
None => {
|
None => {
|
||||||
let amount = sub_command_args
|
let amount = sub_command_args
|
||||||
|
|||||||
209
crates/cdk-cli/src/sub_commands/mint_blind_auth.rs
Normal file
209
crates/cdk-cli/src/sub_commands/mint_blind_auth.rs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use cdk::cdk_database::{Error, WalletDatabase};
|
||||||
|
use cdk::mint_url::MintUrl;
|
||||||
|
use cdk::nuts::{CurrencyUnit, MintInfo};
|
||||||
|
use cdk::wallet::types::WalletKey;
|
||||||
|
use cdk::wallet::{MultiMintWallet, Wallet};
|
||||||
|
use cdk::{Amount, OidcClient};
|
||||||
|
use clap::Args;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::token_storage;
|
||||||
|
|
||||||
|
#[derive(Args, Serialize, Deserialize)]
|
||||||
|
pub struct MintBlindAuthSubCommand {
|
||||||
|
/// Mint url
|
||||||
|
mint_url: MintUrl,
|
||||||
|
/// Amount
|
||||||
|
amount: Option<u64>,
|
||||||
|
/// Cat (access token)
|
||||||
|
#[arg(long)]
|
||||||
|
cat: Option<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<dyn WalletDatabase<Err = Error> + Sync + Send>,
|
||||||
|
sub_command_args: &MintBlindAuthSubCommand,
|
||||||
|
work_dir: &Path,
|
||||||
|
) -> 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)?;
|
||||||
|
|
||||||
|
multi_mint_wallet.add_wallet(wallet.clone()).await;
|
||||||
|
wallet
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wallet.get_mint_info().await?;
|
||||||
|
|
||||||
|
// Try to get the token from the provided argument or from the stored file
|
||||||
|
let cat = match &sub_command_args.cat {
|
||||||
|
Some(token) => token.clone(),
|
||||||
|
None => {
|
||||||
|
// Try to load from file
|
||||||
|
match token_storage::get_token_for_mint(work_dir, &mint_url).await {
|
||||||
|
Ok(Some(token_data)) => {
|
||||||
|
println!("Using access token from cashu_tokens.json");
|
||||||
|
token_data.access_token
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"No access token provided and no token found in cashu_tokens.json for this mint"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to read token from cashu_tokens.json: {}",
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to set the access token
|
||||||
|
if let Err(err) = wallet.set_cat(cat.clone()).await {
|
||||||
|
tracing::error!("Could not set cat: {}", err);
|
||||||
|
|
||||||
|
// Try to refresh the token if we have a refresh token
|
||||||
|
if let Ok(Some(token_data)) = token_storage::get_token_for_mint(work_dir, &mint_url).await {
|
||||||
|
println!("Attempting to refresh the access token...");
|
||||||
|
|
||||||
|
// Get the mint info to access OIDC configuration
|
||||||
|
if let Some(mint_info) = wallet.get_mint_info().await? {
|
||||||
|
match refresh_access_token(&mint_info, &token_data.refresh_token).await {
|
||||||
|
Ok((new_access_token, new_refresh_token)) => {
|
||||||
|
println!("Successfully refreshed access token");
|
||||||
|
|
||||||
|
// Save the new tokens
|
||||||
|
if let Err(e) = token_storage::save_tokens(
|
||||||
|
work_dir,
|
||||||
|
&mint_url,
|
||||||
|
&new_access_token,
|
||||||
|
&new_refresh_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Warning: Failed to save refreshed tokens: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try setting the new access token
|
||||||
|
if let Err(err) = wallet.set_cat(new_access_token).await {
|
||||||
|
tracing::error!("Could not set refreshed cat: {}", err);
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Authentication failed even after token refresh"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the refresh token
|
||||||
|
wallet.set_refresh_token(new_refresh_token).await?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to refresh token: {}", e);
|
||||||
|
return Err(anyhow::anyhow!("Failed to refresh access token: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Authentication failed and no refresh token available"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we have a refresh token, set it
|
||||||
|
if let Ok(Some(token_data)) = token_storage::get_token_for_mint(work_dir, &mint_url).await {
|
||||||
|
tracing::info!("Attempting to use refresh access token to refresh auth token");
|
||||||
|
wallet.set_refresh_token(token_data.refresh_token).await?;
|
||||||
|
wallet.refresh_access_token().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Attempting to mint blind auth");
|
||||||
|
|
||||||
|
let amount = match sub_command_args.amount {
|
||||||
|
Some(amount) => amount,
|
||||||
|
None => {
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await?
|
||||||
|
.ok_or(anyhow!("Unknown mint info"))?;
|
||||||
|
mint_info
|
||||||
|
.bat_max_mint()
|
||||||
|
.ok_or(anyhow!("Unknown max bat mint"))?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let proofs = wallet.mint_blind_auth(Amount::from(amount)).await?;
|
||||||
|
|
||||||
|
println!("Received {} auth proofs for mint {mint_url}", proofs.len());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn refresh_access_token(
|
||||||
|
mint_info: &MintInfo,
|
||||||
|
refresh_token: &str,
|
||||||
|
) -> Result<(String, String)> {
|
||||||
|
let openid_discovery = mint_info
|
||||||
|
.nuts
|
||||||
|
.nut21
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("OIDC discovery information not available"))?
|
||||||
|
.openid_discovery;
|
||||||
|
|
||||||
|
let oidc_client = OidcClient::new(openid_discovery);
|
||||||
|
|
||||||
|
// Get the token endpoint from the OIDC configuration
|
||||||
|
let token_url = oidc_client.get_oidc_config().await?.token_endpoint;
|
||||||
|
|
||||||
|
// Create the request parameters for token refresh
|
||||||
|
let params = [
|
||||||
|
("grant_type", "refresh_token"),
|
||||||
|
("refresh_token", refresh_token),
|
||||||
|
("client_id", "cashu-client"), // Using default client ID
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make the token refresh request
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client.post(token_url).form(¶ms).send().await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Token refresh failed with status: {}",
|
||||||
|
response.status()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_response: serde_json::Value = response.json().await?;
|
||||||
|
|
||||||
|
let access_token = token_response["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No access token in refresh response"))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Get the new refresh token or use the old one if not provided
|
||||||
|
let new_refresh_token = token_response["refresh_token"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or(refresh_token)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok((access_token, new_refresh_token))
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use cdk::mint_url::MintUrl;
|
use cdk::mint_url::MintUrl;
|
||||||
use cdk::wallet::client::MintConnector;
|
use cdk::wallet::MintConnector;
|
||||||
use cdk::HttpClient;
|
use cdk::HttpClient;
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -14,7 +14,7 @@ pub async fn mint_info(proxy: Option<Url>, sub_command_args: &MintInfoSubcommand
|
|||||||
let mint_url = sub_command_args.mint_url.clone();
|
let mint_url = sub_command_args.mint_url.clone();
|
||||||
let client = match proxy {
|
let client = match proxy {
|
||||||
Some(proxy) => HttpClient::with_proxy(mint_url, proxy, None, true)?,
|
Some(proxy) => HttpClient::with_proxy(mint_url, proxy, None, true)?,
|
||||||
None => HttpClient::new(mint_url),
|
None => HttpClient::new(mint_url, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let info = client.get_mint_info().await?;
|
let info = client.get_mint_info().await?;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
pub mod balance;
|
pub mod balance;
|
||||||
pub mod burn;
|
pub mod burn;
|
||||||
|
pub mod cat_device_login;
|
||||||
|
pub mod cat_login;
|
||||||
pub mod check_spent;
|
pub mod check_spent;
|
||||||
pub mod create_request;
|
pub mod create_request;
|
||||||
pub mod decode_request;
|
pub mod decode_request;
|
||||||
@@ -7,6 +9,7 @@ pub mod decode_token;
|
|||||||
pub mod list_mint_proofs;
|
pub mod list_mint_proofs;
|
||||||
pub mod melt;
|
pub mod melt;
|
||||||
pub mod mint;
|
pub mod mint;
|
||||||
|
pub mod mint_blind_auth;
|
||||||
pub mod mint_info;
|
pub mod mint_info;
|
||||||
pub mod pay_request;
|
pub mod pay_request;
|
||||||
pub mod pending_mints;
|
pub mod pending_mints;
|
||||||
|
|||||||
62
crates/cdk-cli/src/token_storage.rs
Normal file
62
crates/cdk-cli/src/token_storage.rs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use cdk::mint_url::MintUrl;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TokenData {
|
||||||
|
pub mint_url: String,
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores authentication tokens in the work directory
|
||||||
|
pub async fn save_tokens(
|
||||||
|
work_dir: &Path,
|
||||||
|
mint_url: &MintUrl,
|
||||||
|
access_token: &str,
|
||||||
|
refresh_token: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let token_data = TokenData {
|
||||||
|
mint_url: mint_url.to_string(),
|
||||||
|
access_token: access_token.to_string(),
|
||||||
|
refresh_token: refresh_token.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&token_data)?;
|
||||||
|
let file_path = work_dir.join(format!(
|
||||||
|
"auth_tokens_{}",
|
||||||
|
mint_url.to_string().replace("/", "_")
|
||||||
|
));
|
||||||
|
let mut file = File::create(file_path)?;
|
||||||
|
file.write_all(json.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets authentication tokens from the work directory
|
||||||
|
pub async fn get_token_for_mint(work_dir: &Path, mint_url: &MintUrl) -> Result<Option<TokenData>> {
|
||||||
|
let file_path = work_dir.join(format!(
|
||||||
|
"auth_tokens_{}",
|
||||||
|
mint_url.to_string().replace("/", "_")
|
||||||
|
));
|
||||||
|
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = File::open(file_path)?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
|
||||||
|
let token_data: TokenData = serde_json::from_str(&contents)?;
|
||||||
|
|
||||||
|
if token_data.mint_url == mint_url.to_string() {
|
||||||
|
Ok(Some(token_data))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ swagger = ["dep:utoipa", "cashu/swagger"]
|
|||||||
bench = []
|
bench = []
|
||||||
wallet = ["cashu/wallet"]
|
wallet = ["cashu/wallet"]
|
||||||
mint = ["cashu/mint", "dep:uuid"]
|
mint = ["cashu/mint", "dep:uuid"]
|
||||||
|
auth = ["cashu/auth"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
|||||||
72
crates/cdk-common/src/database/mint/auth/mod.rs
Normal file
72
crates/cdk-common/src/database/mint/auth/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//! Mint in memory database use std::collections::HashMap;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use cashu::{AuthRequired, ProtectedEndpoint};
|
||||||
|
|
||||||
|
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<Error> + From<Error>;
|
||||||
|
/// 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<Option<Id>, 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<Option<MintKeySetInfo>, Self::Err>;
|
||||||
|
/// Get [`MintKeySetInfo`]s
|
||||||
|
async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, 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<Vec<Option<State>>, Self::Err>;
|
||||||
|
/// Get [`Proofs`] state
|
||||||
|
async fn update_proof_state(
|
||||||
|
&self,
|
||||||
|
y: &PublicKey,
|
||||||
|
proofs_state: State,
|
||||||
|
) -> Result<Option<State>, 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<Vec<Option<BlindSignature>>, Self::Err>;
|
||||||
|
|
||||||
|
/// Add protected endpoints
|
||||||
|
async fn add_protected_endpoints(
|
||||||
|
&self,
|
||||||
|
protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
|
||||||
|
) -> Result<(), Self::Err>;
|
||||||
|
/// Removed Protected endpoints
|
||||||
|
async fn remove_protected_endpoints(
|
||||||
|
&self,
|
||||||
|
protected_endpoints: Vec<ProtectedEndpoint>,
|
||||||
|
) -> Result<(), Self::Err>;
|
||||||
|
/// Get auth for protected_endpoint
|
||||||
|
async fn get_auth_for_endpoint(
|
||||||
|
&self,
|
||||||
|
protected_endpoint: ProtectedEndpoint,
|
||||||
|
) -> Result<Option<AuthRequired>, Self::Err>;
|
||||||
|
/// Get protected endpoints
|
||||||
|
async fn get_auth_for_endpoints(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<ProtectedEndpoint, Option<AuthRequired>>, Self::Err>;
|
||||||
|
}
|
||||||
@@ -14,6 +14,12 @@ use crate::nuts::{
|
|||||||
Proofs, PublicKey, State,
|
Proofs, PublicKey, State,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use auth::MintAuthDatabase;
|
||||||
|
|
||||||
/// Mint Database trait
|
/// Mint Database trait
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Database {
|
pub trait Database {
|
||||||
@@ -7,6 +7,8 @@ mod wallet;
|
|||||||
|
|
||||||
#[cfg(feature = "mint")]
|
#[cfg(feature = "mint")]
|
||||||
pub use mint::Database as MintDatabase;
|
pub use mint::Database as MintDatabase;
|
||||||
|
#[cfg(all(feature = "mint", feature = "auth"))]
|
||||||
|
pub use mint::MintAuthDatabase;
|
||||||
#[cfg(feature = "wallet")]
|
#[cfg(feature = "wallet")]
|
||||||
pub use wallet::Database as WalletDatabase;
|
pub use wallet::Database as WalletDatabase;
|
||||||
|
|
||||||
@@ -25,6 +27,10 @@ pub enum Error {
|
|||||||
/// NUT02 Error
|
/// NUT02 Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NUT02(#[from] crate::nuts::nut02::Error),
|
NUT02(#[from] crate::nuts::nut02::Error),
|
||||||
|
/// NUT22 Error
|
||||||
|
#[error(transparent)]
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
NUT22(#[from] crate::nuts::nut22::Error),
|
||||||
/// Serde Error
|
/// Serde Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Serde(#[from] serde_json::Error),
|
Serde(#[from] serde_json::Error),
|
||||||
|
|||||||
@@ -58,6 +58,36 @@ pub enum Error {
|
|||||||
/// Multi-Part Payment not supported for unit and method
|
/// Multi-Part Payment not supported for unit and method
|
||||||
#[error("Multi-Part payment is not supported for unit `{0}` and method `{1}`")]
|
#[error("Multi-Part payment is not supported for unit `{0}` and method `{1}`")]
|
||||||
MppUnitMethodNotSupported(CurrencyUnit, PaymentMethod),
|
MppUnitMethodNotSupported(CurrencyUnit, PaymentMethod),
|
||||||
|
/// Clear Auth Required
|
||||||
|
#[error("Clear Auth Required")]
|
||||||
|
ClearAuthRequired,
|
||||||
|
/// Blind Auth Required
|
||||||
|
#[error("Blind Auth Required")]
|
||||||
|
BlindAuthRequired,
|
||||||
|
/// Clear Auth Failed
|
||||||
|
#[error("Clear Auth Failed")]
|
||||||
|
ClearAuthFailed,
|
||||||
|
/// Blind Auth Failed
|
||||||
|
#[error("Blind Auth Failed")]
|
||||||
|
BlindAuthFailed,
|
||||||
|
/// Auth settings undefined
|
||||||
|
#[error("Auth settings undefined")]
|
||||||
|
AuthSettingsUndefined,
|
||||||
|
/// 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 undefined")]
|
||||||
|
AuthLocalstoreUndefined,
|
||||||
|
/// Wallet cat not set
|
||||||
|
#[error("Wallet cat not set")]
|
||||||
|
CatNotSet,
|
||||||
|
/// Could not get mint info
|
||||||
|
#[error("Could not get mint info")]
|
||||||
|
CouldNotGetMintInfo,
|
||||||
|
|
||||||
// Mint Errors
|
// Mint Errors
|
||||||
/// Minting is disabled
|
/// Minting is disabled
|
||||||
@@ -126,6 +156,9 @@ pub enum Error {
|
|||||||
/// Internal Error
|
/// Internal Error
|
||||||
#[error("Internal Error")]
|
#[error("Internal Error")]
|
||||||
Internal,
|
Internal,
|
||||||
|
/// Oidc config not set
|
||||||
|
#[error("Oidc client not set")]
|
||||||
|
OidcNotSet,
|
||||||
|
|
||||||
// Wallet Errors
|
// Wallet Errors
|
||||||
/// P2PK spending conditions not met
|
/// P2PK spending conditions not met
|
||||||
@@ -156,6 +189,9 @@ pub enum Error {
|
|||||||
/// Invalid DLEQ proof
|
/// Invalid DLEQ proof
|
||||||
#[error("Could not verify DLEQ proof")]
|
#[error("Could not verify DLEQ proof")]
|
||||||
CouldNotVerifyDleq,
|
CouldNotVerifyDleq,
|
||||||
|
/// Dleq Proof not provided for signature
|
||||||
|
#[error("Dleq proof not provided for signature")]
|
||||||
|
DleqProofNotProvided,
|
||||||
/// Incorrect Mint
|
/// Incorrect Mint
|
||||||
/// Token does not match wallet mint
|
/// Token does not match wallet mint
|
||||||
#[error("Token does not match wallet mint")]
|
#[error("Token does not match wallet mint")]
|
||||||
@@ -213,7 +249,7 @@ pub enum Error {
|
|||||||
/// Http transport error
|
/// Http transport error
|
||||||
#[error("Http transport error: {0}")]
|
#[error("Http transport error: {0}")]
|
||||||
HttpError(String),
|
HttpError(String),
|
||||||
|
#[cfg(feature = "wallet")]
|
||||||
// Crate error conversions
|
// Crate error conversions
|
||||||
/// Cashu Url Error
|
/// Cashu Url Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
@@ -264,6 +300,12 @@ pub enum Error {
|
|||||||
/// NUT20 Error
|
/// NUT20 Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NUT20(#[from] crate::nuts::nut20::Error),
|
NUT20(#[from] crate::nuts::nut20::Error),
|
||||||
|
/// NUTXX Error
|
||||||
|
#[error(transparent)]
|
||||||
|
NUT21(#[from] crate::nuts::nut21::Error),
|
||||||
|
/// NUTXX1 Error
|
||||||
|
#[error(transparent)]
|
||||||
|
NUT22(#[from] crate::nuts::nut22::Error),
|
||||||
/// Database Error
|
/// Database Error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Database(#[from] crate::database::Error),
|
Database(#[from] crate::database::Error),
|
||||||
@@ -397,6 +439,26 @@ impl From<Error> for ErrorResponse {
|
|||||||
error: Some(err.to_string()),
|
error: Some(err.to_string()),
|
||||||
detail: None,
|
detail: None,
|
||||||
},
|
},
|
||||||
|
Error::ClearAuthRequired => ErrorResponse {
|
||||||
|
code: ErrorCode::ClearAuthRequired,
|
||||||
|
error: None,
|
||||||
|
detail: None,
|
||||||
|
},
|
||||||
|
Error::ClearAuthFailed => ErrorResponse {
|
||||||
|
code: ErrorCode::ClearAuthFailed,
|
||||||
|
error: None,
|
||||||
|
detail: None,
|
||||||
|
},
|
||||||
|
Error::BlindAuthRequired => ErrorResponse {
|
||||||
|
code: ErrorCode::BlindAuthRequired,
|
||||||
|
error: None,
|
||||||
|
detail: None,
|
||||||
|
},
|
||||||
|
Error::BlindAuthFailed => ErrorResponse {
|
||||||
|
code: ErrorCode::BlindAuthFailed,
|
||||||
|
error: None,
|
||||||
|
detail: None,
|
||||||
|
},
|
||||||
Error::NUT20(err) => ErrorResponse {
|
Error::NUT20(err) => ErrorResponse {
|
||||||
code: ErrorCode::WitnessMissingOrInvalid,
|
code: ErrorCode::WitnessMissingOrInvalid,
|
||||||
error: Some(err.to_string()),
|
error: Some(err.to_string()),
|
||||||
@@ -456,6 +518,8 @@ impl From<ErrorResponse> for Error {
|
|||||||
ErrorCode::DuplicateOutputs => Self::DuplicateOutputs,
|
ErrorCode::DuplicateOutputs => Self::DuplicateOutputs,
|
||||||
ErrorCode::MultipleUnits => Self::MultipleUnits,
|
ErrorCode::MultipleUnits => Self::MultipleUnits,
|
||||||
ErrorCode::UnitMismatch => Self::UnitMismatch,
|
ErrorCode::UnitMismatch => Self::UnitMismatch,
|
||||||
|
ErrorCode::ClearAuthRequired => Self::ClearAuthRequired,
|
||||||
|
ErrorCode::BlindAuthRequired => Self::BlindAuthRequired,
|
||||||
_ => Self::UnknownErrorResponse(err.to_string()),
|
_ => Self::UnknownErrorResponse(err.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,6 +571,14 @@ pub enum ErrorCode {
|
|||||||
MultipleUnits,
|
MultipleUnits,
|
||||||
/// Input unit does not match output
|
/// Input unit does not match output
|
||||||
UnitMismatch,
|
UnitMismatch,
|
||||||
|
/// Clear Auth Required
|
||||||
|
ClearAuthRequired,
|
||||||
|
/// Clear Auth Failed
|
||||||
|
ClearAuthFailed,
|
||||||
|
/// Blind Auth Required
|
||||||
|
BlindAuthRequired,
|
||||||
|
/// Blind Auth Failed
|
||||||
|
BlindAuthFailed,
|
||||||
/// Unknown error code
|
/// Unknown error code
|
||||||
Unknown(u16),
|
Unknown(u16),
|
||||||
}
|
}
|
||||||
@@ -536,6 +608,10 @@ impl ErrorCode {
|
|||||||
20006 => Self::InvoiceAlreadyPaid,
|
20006 => Self::InvoiceAlreadyPaid,
|
||||||
20007 => Self::QuoteExpired,
|
20007 => Self::QuoteExpired,
|
||||||
20008 => Self::WitnessMissingOrInvalid,
|
20008 => Self::WitnessMissingOrInvalid,
|
||||||
|
30001 => Self::ClearAuthRequired,
|
||||||
|
30002 => Self::ClearAuthFailed,
|
||||||
|
31001 => Self::BlindAuthRequired,
|
||||||
|
31002 => Self::BlindAuthFailed,
|
||||||
_ => Self::Unknown(code),
|
_ => Self::Unknown(code),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -564,6 +640,10 @@ impl ErrorCode {
|
|||||||
Self::InvoiceAlreadyPaid => 20006,
|
Self::InvoiceAlreadyPaid => 20006,
|
||||||
Self::QuoteExpired => 20007,
|
Self::QuoteExpired => 20007,
|
||||||
Self::WitnessMissingOrInvalid => 20008,
|
Self::WitnessMissingOrInvalid => 20008,
|
||||||
|
Self::ClearAuthRequired => 30001,
|
||||||
|
Self::ClearAuthFailed => 30002,
|
||||||
|
Self::BlindAuthRequired => 31001,
|
||||||
|
Self::BlindAuthFailed => 31002,
|
||||||
Self::Unknown(code) => *code,
|
Self::Unknown(code) => *code,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ pub mod payment;
|
|||||||
pub mod pub_sub;
|
pub mod pub_sub;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
// re-exporting external crates
|
// re-exporting external crates
|
||||||
pub use bitcoin;
|
pub use bitcoin;
|
||||||
pub use cashu::amount::{self, Amount};
|
pub use cashu::amount::{self, Amount};
|
||||||
@@ -25,3 +24,4 @@ pub use cashu::nuts::{self, *};
|
|||||||
#[cfg(feature = "wallet")]
|
#[cfg(feature = "wallet")]
|
||||||
pub use cashu::wallet;
|
pub use cashu::wallet;
|
||||||
pub use cashu::{dhke, ensure_cdk, mint_url, secret, util, SECP256K1};
|
pub use cashu::{dhke, ensure_cdk, mint_url, secret, util, SECP256K1};
|
||||||
|
pub use error::Error;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ rand.workspace = true
|
|||||||
bip39 = { workspace = true, features = ["rand"] }
|
bip39 = { workspace = true, features = ["rand"] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
cashu = { path = "../cashu", features = ["mint", "wallet"] }
|
cashu = { path = "../cashu", features = ["mint", "wallet"] }
|
||||||
cdk = { path = "../cdk", features = ["mint", "wallet"] }
|
cdk = { path = "../cdk", features = ["mint", "wallet", "auth"] }
|
||||||
cdk-cln = { path = "../cdk-cln" }
|
cdk-cln = { path = "../cdk-cln" }
|
||||||
cdk-lnd = { path = "../cdk-lnd" }
|
cdk-lnd = { path = "../cdk-lnd" }
|
||||||
cdk-axum = { path = "../cdk-axum" }
|
cdk-axum = { path = "../cdk-axum" }
|
||||||
@@ -42,6 +42,7 @@ tracing-subscriber.workspace = true
|
|||||||
tokio-tungstenite.workspace = true
|
tokio-tungstenite.workspace = true
|
||||||
tower-http = { workspace = true, features = ["cors"] }
|
tower-http = { workspace = true, features = ["cors"] }
|
||||||
tower-service = "0.3.3"
|
tower-service = "0.3.3"
|
||||||
|
reqwest.workspace = true
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|||||||
102
crates/cdk-integration-tests/src/init_auth_mint.rs
Normal file
102
crates/cdk-integration-tests/src/init_auth_mint.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use bip39::Mnemonic;
|
||||||
|
use cashu::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
|
||||||
|
use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase};
|
||||||
|
use cdk::mint::{MintBuilder, MintMeltLimits};
|
||||||
|
use cdk::nuts::{CurrencyUnit, PaymentMethod};
|
||||||
|
use cdk::types::FeeReserve;
|
||||||
|
use cdk::wallet::AuthWallet;
|
||||||
|
use cdk_fake_wallet::FakeWallet;
|
||||||
|
|
||||||
|
pub async fn start_fake_mint_with_auth<D, A>(
|
||||||
|
_addr: &str,
|
||||||
|
_port: u16,
|
||||||
|
openid_discovery: String,
|
||||||
|
database: D,
|
||||||
|
auth_database: A,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
|
||||||
|
A: MintAuthDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let fee_reserve = FeeReserve {
|
||||||
|
min_fee_reserve: 1.into(),
|
||||||
|
percent_fee_reserve: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0);
|
||||||
|
|
||||||
|
let mut mint_builder = MintBuilder::new();
|
||||||
|
|
||||||
|
mint_builder = mint_builder.with_localstore(Arc::new(database));
|
||||||
|
|
||||||
|
mint_builder = mint_builder
|
||||||
|
.add_ln_backend(
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
PaymentMethod::Bolt11,
|
||||||
|
MintMeltLimits::new(1, 300),
|
||||||
|
Arc::new(fake_wallet),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
mint_builder =
|
||||||
|
mint_builder.set_clear_auth_settings(openid_discovery, "cashu-client".to_string());
|
||||||
|
|
||||||
|
mint_builder = mint_builder.set_blind_auth_settings(50);
|
||||||
|
|
||||||
|
let blind_auth_endpoints = vec![
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::MintQuoteBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::MintBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MintQuoteBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::MeltQuoteBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Get, RoutePath::MeltQuoteBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::MeltBolt11),
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::Checkstate),
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::Restore),
|
||||||
|
];
|
||||||
|
|
||||||
|
let blind_auth_endpoints =
|
||||||
|
blind_auth_endpoints
|
||||||
|
.into_iter()
|
||||||
|
.fold(HashMap::new(), |mut acc, e| {
|
||||||
|
acc.insert(e, AuthRequired::Blind);
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
auth_database
|
||||||
|
.add_protected_endpoints(blind_auth_endpoints)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut clear_auth_endpoint = HashMap::new();
|
||||||
|
clear_auth_endpoint.insert(
|
||||||
|
ProtectedEndpoint::new(Method::Post, RoutePath::MintBlindAuth),
|
||||||
|
AuthRequired::Clear,
|
||||||
|
);
|
||||||
|
|
||||||
|
auth_database
|
||||||
|
.add_protected_endpoints(clear_auth_endpoint)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
mint_builder = mint_builder.with_auth_localstore(Arc::new(auth_database));
|
||||||
|
|
||||||
|
let mnemonic = Mnemonic::generate(12)?;
|
||||||
|
|
||||||
|
mint_builder = mint_builder
|
||||||
|
.with_description("fake test mint".to_string())
|
||||||
|
.with_seed(mnemonic.to_seed_normalized("").to_vec());
|
||||||
|
|
||||||
|
let _mint = mint_builder.build().await?;
|
||||||
|
|
||||||
|
todo!("Need to start this a cdk mintd keeping as ref for now");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn top_up_blind_auth_proofs(auth_wallet: Arc<AuthWallet>, count: u64) {
|
||||||
|
let _proofs = auth_wallet
|
||||||
|
.mint_blind_auth(count.into())
|
||||||
|
.await
|
||||||
|
.expect("could not mint blind auth");
|
||||||
|
}
|
||||||
@@ -18,11 +18,10 @@ use cdk::nuts::{
|
|||||||
};
|
};
|
||||||
use cdk::types::{FeeReserve, QuoteTTL};
|
use cdk::types::{FeeReserve, QuoteTTL};
|
||||||
use cdk::util::unix_time;
|
use cdk::util::unix_time;
|
||||||
use cdk::wallet::client::MintConnector;
|
use cdk::wallet::{AuthWallet, MintConnector, Wallet, WalletBuilder};
|
||||||
use cdk::wallet::Wallet;
|
|
||||||
use cdk::{Amount, Error, Mint};
|
use cdk::{Amount, Error, Mint};
|
||||||
use cdk_fake_wallet::FakeWallet;
|
use cdk_fake_wallet::FakeWallet;
|
||||||
use tokio::sync::{Mutex, Notify};
|
use tokio::sync::{Mutex, Notify, RwLock};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -30,11 +29,15 @@ use crate::wait_for_mint_to_be_paid;
|
|||||||
|
|
||||||
pub struct DirectMintConnection {
|
pub struct DirectMintConnection {
|
||||||
pub mint: Arc<Mint>,
|
pub mint: Arc<Mint>,
|
||||||
|
auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DirectMintConnection {
|
impl DirectMintConnection {
|
||||||
pub fn new(mint: Arc<Mint>) -> Self {
|
pub fn new(mint: Arc<Mint>) -> Self {
|
||||||
Self { mint }
|
Self {
|
||||||
|
mint,
|
||||||
|
auth_wallet: Arc::new(RwLock::new(None)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +144,18 @@ impl MintConnector for DirectMintConnection {
|
|||||||
async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
|
async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
|
||||||
self.mint.restore(request).await
|
self.mint.restore(request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the auth wallet for the client
|
||||||
|
async fn get_auth_wallet(&self) -> Option<AuthWallet> {
|
||||||
|
self.auth_wallet.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set auth wallet on client
|
||||||
|
async fn set_auth_wallet(&self, wallet: Option<AuthWallet>) {
|
||||||
|
let mut auth_wallet = self.auth_wallet.write().await;
|
||||||
|
|
||||||
|
*auth_wallet = wallet;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_tracing() {
|
pub fn setup_tracing() {
|
||||||
@@ -232,9 +247,14 @@ async fn create_test_wallet_for_mint(mint: Arc<Mint>) -> Result<Wallet> {
|
|||||||
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
let seed = Mnemonic::generate(12)?.to_seed_normalized("");
|
||||||
let unit = CurrencyUnit::Sat;
|
let unit = CurrencyUnit::Sat;
|
||||||
let localstore = cdk_sqlite::wallet::memory::empty().await?;
|
let localstore = cdk_sqlite::wallet::memory::empty().await?;
|
||||||
let mut wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?;
|
|
||||||
|
|
||||||
wallet.set_client(connector);
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(mint_url.parse().unwrap())
|
||||||
|
.unit(unit)
|
||||||
|
.localstore(Arc::new(localstore))
|
||||||
|
.seed(&seed)
|
||||||
|
.client(connector)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
Ok(wallet)
|
Ok(wallet)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,29 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use cdk::amount::{Amount, SplitTarget};
|
use cdk::amount::{Amount, SplitTarget};
|
||||||
use cdk::nuts::nut00::ProofsMethods;
|
|
||||||
use cdk::nuts::{MintQuoteState, NotificationPayload, State};
|
use cdk::nuts::{MintQuoteState, NotificationPayload, State};
|
||||||
use cdk::wallet::WalletSubscription;
|
use cdk::wallet::WalletSubscription;
|
||||||
use cdk::Wallet;
|
use cdk::Wallet;
|
||||||
use tokio::time::{sleep, timeout, Duration};
|
use tokio::time::{sleep, timeout, Duration};
|
||||||
|
|
||||||
|
pub mod init_auth_mint;
|
||||||
pub mod init_pure_tests;
|
pub mod init_pure_tests;
|
||||||
pub mod init_regtest;
|
pub mod init_regtest;
|
||||||
|
|
||||||
pub async fn wallet_mint(
|
pub async fn fund_wallet(wallet: Arc<Wallet>, amount: Amount) {
|
||||||
wallet: Arc<Wallet>,
|
let quote = wallet
|
||||||
amount: Amount,
|
.mint_quote(amount, None)
|
||||||
split_target: SplitTarget,
|
.await
|
||||||
description: Option<String>,
|
.expect("Could not get mint quote");
|
||||||
) -> Result<()> {
|
|
||||||
let quote = wallet.mint_quote(amount, description).await?;
|
|
||||||
|
|
||||||
wait_for_mint_to_be_paid(&wallet, "e.id, 60).await?;
|
wait_for_mint_to_be_paid(&wallet, "e.id, 60)
|
||||||
|
.await
|
||||||
|
.expect("Waiting for mint failed");
|
||||||
|
|
||||||
let proofs = wallet.mint("e.id, split_target, None).await?;
|
let _proofs = wallet
|
||||||
|
.mint("e.id, SplitTarget::default(), None)
|
||||||
let receive_amount = proofs.total_amount()?;
|
.await
|
||||||
|
.expect("Could not mint");
|
||||||
println!("Minted: {}", receive_amount);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all pending from wallet and attempt to swap
|
// Get all pending from wallet and attempt to swap
|
||||||
|
|||||||
39
crates/cdk-integration-tests/src/mock_oauth/mod.rs
Normal file
39
crates/cdk-integration-tests/src/mock_oauth/mod.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use axum::response::{IntoResponse, Response, Result};
|
||||||
|
use axum::routing::get;
|
||||||
|
use axum::{Json, Router};
|
||||||
|
use cdk::oidc_client::OidcConfig;
|
||||||
|
use jsonwebtoken::jwk::{AlgorithmParameters, Jwk, JwkSet};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
async fn crate_mock_oauth() -> Router {
|
||||||
|
let router = Router::new()
|
||||||
|
.route("/config", get(handler_get_config))
|
||||||
|
.route("/token", get(handler_get_token))
|
||||||
|
.route("/jwks", get(handler_get_jwkset));
|
||||||
|
router
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler_get_config() -> Result<Json<OidcConfig>> {
|
||||||
|
Ok(Json(OidcConfig {
|
||||||
|
jwks_uri: "/jwks".to_string(),
|
||||||
|
issuer: "127.0.0.1".to_string(),
|
||||||
|
token_endpoint: "/token".to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler_get_jwkset() -> Result<Json<JwkSet>> {
|
||||||
|
let jwk:Jwk = serde_json::from_value(json!({
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ",
|
||||||
|
"e": "AQAB",
|
||||||
|
"kid": "rsa01",
|
||||||
|
"alg": "RS256",
|
||||||
|
"use": "sig"
|
||||||
|
})).unwrap();
|
||||||
|
|
||||||
|
Ok(Json(JwkSet { keys: vec![jwk] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler_get_token() -> Result<Json<Value>> {
|
||||||
|
Ok(Json(json!({"access_token": ""})))
|
||||||
|
}
|
||||||
866
crates/cdk-integration-tests/tests/fake_auth.rs
Normal file
866
crates/cdk-integration-tests/tests/fake_auth.rs
Normal file
@@ -0,0 +1,866 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use bip39::Mnemonic;
|
||||||
|
use cashu::{MintAuthRequest, MintInfo};
|
||||||
|
use cdk::amount::{Amount, SplitTarget};
|
||||||
|
use cdk::mint_url::MintUrl;
|
||||||
|
use cdk::nuts::nut00::ProofsMethods;
|
||||||
|
use cdk::nuts::{
|
||||||
|
AuthProof, AuthToken, BlindAuthToken, CheckStateRequest, CurrencyUnit, MeltBolt11Request,
|
||||||
|
MeltQuoteBolt11Request, MeltQuoteState, MintBolt11Request, MintQuoteBolt11Request,
|
||||||
|
RestoreRequest, State, SwapRequest,
|
||||||
|
};
|
||||||
|
use cdk::wallet::{AuthHttpClient, AuthMintConnector, HttpClient, MintConnector, WalletBuilder};
|
||||||
|
use cdk::{Error, OidcClient};
|
||||||
|
use cdk_fake_wallet::create_fake_invoice;
|
||||||
|
use cdk_integration_tests::{fund_wallet, wait_for_mint_to_be_paid};
|
||||||
|
use cdk_sqlite::wallet::memory;
|
||||||
|
|
||||||
|
const MINT_URL: &str = "http://127.0.0.1:8087";
|
||||||
|
const ENV_OIDC_USER: &str = "CDK_TEST_OIDC_USER";
|
||||||
|
const ENV_OIDC_PASSWORD: &str = "CDK_TEST_OIDC_PASSWORD";
|
||||||
|
|
||||||
|
fn get_oidc_credentials() -> (String, String) {
|
||||||
|
let user = env::var(ENV_OIDC_USER).unwrap_or_else(|_| "test".to_string());
|
||||||
|
let password = env::var(ENV_OIDC_PASSWORD).unwrap_or_else(|_| "test".to_string());
|
||||||
|
(user, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_invalid_credentials() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await
|
||||||
|
.expect("mint info")
|
||||||
|
.expect("could not get mint info");
|
||||||
|
|
||||||
|
// Try to get a token with invalid credentials
|
||||||
|
let token_result =
|
||||||
|
get_custom_access_token(&mint_info, "invalid_user", "invalid_password").await;
|
||||||
|
|
||||||
|
// Should fail with an error
|
||||||
|
assert!(
|
||||||
|
token_result.is_err(),
|
||||||
|
"Expected authentication to fail with invalid credentials"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_quote_status_without_auth() {
|
||||||
|
let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
|
||||||
|
|
||||||
|
// Test mint quote status
|
||||||
|
{
|
||||||
|
let quote_res = client
|
||||||
|
.get_mint_quote_status("123e4567-e89b-12d3-a456-426614174000")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test melt quote status
|
||||||
|
{
|
||||||
|
let quote_res = client
|
||||||
|
.get_melt_quote_status("123e4567-e89b-12d3-a456-426614174000")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_mint_without_auth() {
|
||||||
|
let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
|
||||||
|
{
|
||||||
|
let request = MintQuoteBolt11Request {
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
amount: 10.into(),
|
||||||
|
description: None,
|
||||||
|
pubkey: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote_res = client.post_mint_quote(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let request = MintBolt11Request {
|
||||||
|
quote: "123e4567-e89b-12d3-a456-426614174000".to_string(),
|
||||||
|
outputs: vec![],
|
||||||
|
signature: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mint_res = client.post_mint(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(mint_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
mint_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mint_res = client
|
||||||
|
.get_mint_quote_status("123e4567-e89b-12d3-a456-426614174000")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(mint_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
mint_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_mint_bat_without_cat() {
|
||||||
|
let client = AuthHttpClient::new(MintUrl::from_str(MINT_URL).expect("valid mint url"), None);
|
||||||
|
|
||||||
|
let res = client
|
||||||
|
.post_mint_blind_auth(MintAuthRequest { outputs: vec![] })
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(res, Err(Error::ClearAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_swap_without_auth() {
|
||||||
|
let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
|
||||||
|
|
||||||
|
let request = SwapRequest {
|
||||||
|
inputs: vec![],
|
||||||
|
outputs: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote_res = client.post_swap(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_melt_without_auth() {
|
||||||
|
let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
|
||||||
|
|
||||||
|
// Test melt quote request
|
||||||
|
{
|
||||||
|
let request = MeltQuoteBolt11Request {
|
||||||
|
request: create_fake_invoice(100, "".to_string()),
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
options: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote_res = client.post_melt_quote(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test melt quote
|
||||||
|
{
|
||||||
|
let request = MeltQuoteBolt11Request {
|
||||||
|
request: create_fake_invoice(100, "".to_string()),
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
options: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote_res = client.post_melt_quote(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test melt
|
||||||
|
{
|
||||||
|
let request = MeltBolt11Request {
|
||||||
|
inputs: vec![],
|
||||||
|
outputs: None,
|
||||||
|
quote: "123e4567-e89b-12d3-a456-426614174000".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let melt_res = client.post_melt(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(melt_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
melt_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check melt quote state
|
||||||
|
{
|
||||||
|
let melt_res = client
|
||||||
|
.get_melt_quote_status("123e4567-e89b-12d3-a456-426614174000")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(melt_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
melt_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_check_without_auth() {
|
||||||
|
let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
|
||||||
|
|
||||||
|
let request = CheckStateRequest { ys: vec![] };
|
||||||
|
|
||||||
|
let quote_res = client.post_check_state(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_restore_without_auth() {
|
||||||
|
let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
|
||||||
|
|
||||||
|
let request = RestoreRequest { outputs: vec![] };
|
||||||
|
|
||||||
|
let restore_res = client.post_restore(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(restore_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
restore_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_mint_blind_auth() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
let mint_info = wallet.get_mint_info().await.unwrap().unwrap();
|
||||||
|
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
wallet
|
||||||
|
.mint_blind_auth(10.into())
|
||||||
|
.await
|
||||||
|
.expect("Could not mint blind auth");
|
||||||
|
|
||||||
|
let proofs = wallet
|
||||||
|
.get_unspent_auth_proofs()
|
||||||
|
.await
|
||||||
|
.expect("Could not get auth proofs");
|
||||||
|
|
||||||
|
assert!(proofs.len() == 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_mint_with_auth() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await
|
||||||
|
.expect("mint info")
|
||||||
|
.expect("could not get mint info");
|
||||||
|
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
println!("st{}", access_token);
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
wallet
|
||||||
|
.mint_blind_auth(10.into())
|
||||||
|
.await
|
||||||
|
.expect("Could not mint blind auth");
|
||||||
|
|
||||||
|
let wallet = Arc::new(wallet);
|
||||||
|
|
||||||
|
let mint_amount: Amount = 100.into();
|
||||||
|
|
||||||
|
let mint_quote = wallet
|
||||||
|
.mint_quote(mint_amount, None)
|
||||||
|
.await
|
||||||
|
.expect("failed to get mint quote");
|
||||||
|
|
||||||
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60)
|
||||||
|
.await
|
||||||
|
.expect("failed to wait for payment");
|
||||||
|
|
||||||
|
let proofs = wallet
|
||||||
|
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||||
|
.await
|
||||||
|
.expect("could not mint");
|
||||||
|
|
||||||
|
assert!(proofs.total_amount().expect("Could not get proofs amount") == mint_amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_swap_with_auth() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
let mint_info = wallet.get_mint_info().await.unwrap().unwrap();
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
let wallet = Arc::new(wallet);
|
||||||
|
|
||||||
|
wallet.mint_blind_auth(10.into()).await.unwrap();
|
||||||
|
|
||||||
|
fund_wallet(wallet.clone(), 100.into()).await;
|
||||||
|
|
||||||
|
let proofs = wallet
|
||||||
|
.get_unspent_proofs()
|
||||||
|
.await
|
||||||
|
.expect("Could not get proofs");
|
||||||
|
|
||||||
|
let swapped_proofs = wallet
|
||||||
|
.swap(
|
||||||
|
Some(proofs.total_amount().unwrap()),
|
||||||
|
SplitTarget::default(),
|
||||||
|
proofs.clone(),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Could not swap")
|
||||||
|
.expect("Could not swap");
|
||||||
|
|
||||||
|
let check_spent = wallet
|
||||||
|
.check_proofs_spent(proofs.clone())
|
||||||
|
.await
|
||||||
|
.expect("Could not check proofs");
|
||||||
|
|
||||||
|
for state in check_spent {
|
||||||
|
if state.state != State::Spent {
|
||||||
|
panic!("Input proofs should be spent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(swapped_proofs.total_amount().unwrap() == proofs.total_amount().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_melt_with_auth() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await
|
||||||
|
.expect("Mint info not found")
|
||||||
|
.expect("Mint info not found");
|
||||||
|
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
let wallet = Arc::new(wallet);
|
||||||
|
|
||||||
|
wallet.mint_blind_auth(10.into()).await.unwrap();
|
||||||
|
|
||||||
|
fund_wallet(wallet.clone(), 100.into()).await;
|
||||||
|
|
||||||
|
let bolt11 = create_fake_invoice(2_000, "".to_string());
|
||||||
|
|
||||||
|
let melt_quote = wallet
|
||||||
|
.melt_quote(bolt11.to_string(), None)
|
||||||
|
.await
|
||||||
|
.expect("Could not get melt quote");
|
||||||
|
|
||||||
|
let after_melt = wallet.melt(&melt_quote.id).await.expect("Could not melt");
|
||||||
|
|
||||||
|
assert!(after_melt.state == MeltQuoteState::Paid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_mint_auth_over_max() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
|
||||||
|
let wallet = Arc::new(wallet);
|
||||||
|
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await
|
||||||
|
.expect("Mint info not found")
|
||||||
|
.expect("Mint info not found");
|
||||||
|
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
let auth_proofs = wallet
|
||||||
|
.mint_blind_auth((mint_info.nuts.nut22.expect("Auth enabled").bat_max_mint + 1).into())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
auth_proofs,
|
||||||
|
Err(Error::AmountOutofLimitRange(
|
||||||
|
Amount::ZERO,
|
||||||
|
Amount::ZERO,
|
||||||
|
Amount::ZERO,
|
||||||
|
))
|
||||||
|
),
|
||||||
|
"Expected amount out of limit error, got {:?}",
|
||||||
|
auth_proofs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_reuse_auth_proof() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
let mint_info = wallet.get_mint_info().await.unwrap().unwrap();
|
||||||
|
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
wallet.mint_blind_auth(1.into()).await.unwrap();
|
||||||
|
|
||||||
|
let proofs = wallet
|
||||||
|
.localstore
|
||||||
|
.get_proofs(None, Some(CurrencyUnit::Auth), None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(proofs.len() == 1);
|
||||||
|
|
||||||
|
{
|
||||||
|
let quote = wallet
|
||||||
|
.mint_quote(10.into(), None)
|
||||||
|
.await
|
||||||
|
.expect("Quote should be allowed");
|
||||||
|
|
||||||
|
assert!(quote.amount == 10.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet
|
||||||
|
.localstore
|
||||||
|
.update_proofs(proofs, vec![])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let quote_res = wallet.mint_quote(10.into(), None).await;
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::TokenAlreadySpent)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_melt_with_invalid_auth() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
let mint_info = wallet.get_mint_info().await.unwrap().unwrap();
|
||||||
|
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
wallet.mint_blind_auth(10.into()).await.unwrap();
|
||||||
|
|
||||||
|
fund_wallet(Arc::new(wallet.clone()), 1.into()).await;
|
||||||
|
|
||||||
|
let proofs = wallet
|
||||||
|
.get_unspent_proofs()
|
||||||
|
.await
|
||||||
|
.expect("wallet has proofs");
|
||||||
|
|
||||||
|
println!("{:#?}", proofs);
|
||||||
|
let proof = proofs.first().expect("wallet has one proof");
|
||||||
|
|
||||||
|
let client = HttpClient::new(MintUrl::from_str(MINT_URL).expect("Valid mint url"), None);
|
||||||
|
{
|
||||||
|
let invalid_auth_proof = AuthProof {
|
||||||
|
keyset_id: proof.keyset_id,
|
||||||
|
secret: proof.secret.clone(),
|
||||||
|
c: proof.c,
|
||||||
|
dleq: proof.dleq.clone().unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _auth_token = AuthToken::BlindAuth(BlindAuthToken::new(invalid_auth_proof));
|
||||||
|
|
||||||
|
let request = MintQuoteBolt11Request {
|
||||||
|
unit: CurrencyUnit::Sat,
|
||||||
|
amount: 10.into(),
|
||||||
|
description: None,
|
||||||
|
pubkey: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let quote_res = client.post_mint_quote(request).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(quote_res, Err(Error::BlindAuthRequired)),
|
||||||
|
"Expected AuthRequired error, got {:?}",
|
||||||
|
quote_res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_refresh_access_token() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await
|
||||||
|
.expect("mint info")
|
||||||
|
.expect("could not get mint info");
|
||||||
|
|
||||||
|
let (access_token, refresh_token) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
// Set the initial access token and refresh token
|
||||||
|
wallet.set_cat(access_token.clone()).await.unwrap();
|
||||||
|
wallet
|
||||||
|
.set_refresh_token(refresh_token.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Mint some blind auth tokens with the initial access token
|
||||||
|
wallet.mint_blind_auth(5.into()).await.unwrap();
|
||||||
|
|
||||||
|
// Refresh the access token
|
||||||
|
wallet.refresh_access_token().await.unwrap();
|
||||||
|
|
||||||
|
// Verify we can still perform operations with the refreshed token
|
||||||
|
let mint_amount: Amount = 10.into();
|
||||||
|
|
||||||
|
// Try to mint more blind auth tokens with the refreshed token
|
||||||
|
let auth_proofs = wallet.mint_blind_auth(5.into()).await.unwrap();
|
||||||
|
assert_eq!(auth_proofs.len(), 5);
|
||||||
|
|
||||||
|
let total_auth_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
|
||||||
|
assert_eq!(total_auth_proofs.len(), 10); // 5 from before refresh + 5 after refresh
|
||||||
|
|
||||||
|
// Try to get a mint quote with the refreshed token
|
||||||
|
let mint_quote = wallet
|
||||||
|
.mint_quote(mint_amount, None)
|
||||||
|
.await
|
||||||
|
.expect("failed to get mint quote with refreshed token");
|
||||||
|
|
||||||
|
assert_eq!(mint_quote.amount, mint_amount);
|
||||||
|
|
||||||
|
// Verify the total number of auth tokens
|
||||||
|
let total_auth_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
|
||||||
|
assert_eq!(total_auth_proofs.len(), 9); // 5 from before refresh + 5 after refresh - 1 for the quote
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_invalid_refresh_token() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await
|
||||||
|
.expect("mint info")
|
||||||
|
.expect("could not get mint info");
|
||||||
|
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
// Set the initial access token
|
||||||
|
wallet.set_cat(access_token.clone()).await.unwrap();
|
||||||
|
|
||||||
|
// Set an invalid refresh token
|
||||||
|
wallet
|
||||||
|
.set_refresh_token("invalid_refresh_token".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Attempt to refresh the access token with an invalid refresh token
|
||||||
|
let refresh_result = wallet.refresh_access_token().await;
|
||||||
|
|
||||||
|
// Should fail with an error
|
||||||
|
assert!(refresh_result.is_err(), "Expected refresh token error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||||
|
async fn test_auth_token_spending_order() {
|
||||||
|
let db = Arc::new(memory::empty().await.unwrap());
|
||||||
|
|
||||||
|
let wallet = WalletBuilder::new()
|
||||||
|
.mint_url(MintUrl::from_str(MINT_URL).expect("Valid mint url"))
|
||||||
|
.unit(CurrencyUnit::Sat)
|
||||||
|
.localstore(db.clone())
|
||||||
|
.seed(&Mnemonic::generate(12).unwrap().to_seed_normalized(""))
|
||||||
|
.build()
|
||||||
|
.expect("Wallet");
|
||||||
|
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await
|
||||||
|
.expect("mint info")
|
||||||
|
.expect("could not get mint info");
|
||||||
|
|
||||||
|
let (access_token, _) = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
// Mint auth tokens in two batches to test ordering
|
||||||
|
wallet.mint_blind_auth(2.into()).await.unwrap();
|
||||||
|
|
||||||
|
// Get the first batch of auth proofs
|
||||||
|
let first_batch = wallet.get_unspent_auth_proofs().await.unwrap();
|
||||||
|
assert_eq!(first_batch.len(), 2);
|
||||||
|
|
||||||
|
// Mint a second batch
|
||||||
|
wallet.mint_blind_auth(3.into()).await.unwrap();
|
||||||
|
|
||||||
|
// Get all auth proofs
|
||||||
|
let all_proofs = wallet.get_unspent_auth_proofs().await.unwrap();
|
||||||
|
assert_eq!(all_proofs.len(), 5);
|
||||||
|
|
||||||
|
// Use tokens and verify they're used in the expected order (FIFO)
|
||||||
|
for i in 0..3 {
|
||||||
|
let mint_quote = wallet
|
||||||
|
.mint_quote(10.into(), None)
|
||||||
|
.await
|
||||||
|
.expect("failed to get mint quote");
|
||||||
|
|
||||||
|
assert_eq!(mint_quote.amount, 10.into());
|
||||||
|
|
||||||
|
// Check remaining tokens after each operation
|
||||||
|
let remaining = wallet.get_unspent_auth_proofs().await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
remaining.len(),
|
||||||
|
5 - (i + 1),
|
||||||
|
"Expected {} remaining auth tokens after {} operations",
|
||||||
|
5 - (i + 1),
|
||||||
|
i + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_access_token(mint_info: &MintInfo) -> (String, String) {
|
||||||
|
let openid_discovery = mint_info
|
||||||
|
.nuts
|
||||||
|
.nut21
|
||||||
|
.clone()
|
||||||
|
.expect("Nutxx defined")
|
||||||
|
.openid_discovery;
|
||||||
|
|
||||||
|
let oidc_client = OidcClient::new(openid_discovery);
|
||||||
|
|
||||||
|
// Get the token endpoint from the OIDC configuration
|
||||||
|
let token_url = oidc_client
|
||||||
|
.get_oidc_config()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get OIDC config")
|
||||||
|
.token_endpoint;
|
||||||
|
|
||||||
|
// Create the request parameters
|
||||||
|
let (user, password) = get_oidc_credentials();
|
||||||
|
let params = [
|
||||||
|
("grant_type", "password"),
|
||||||
|
("client_id", "cashu-client"),
|
||||||
|
("username", &user),
|
||||||
|
("password", &password),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make the token request directly
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.post(token_url)
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send token request");
|
||||||
|
|
||||||
|
let token_response: serde_json::Value = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse token response");
|
||||||
|
|
||||||
|
let access_token = token_response["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No access token in response")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let refresh_token = token_response["refresh_token"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No access token in response")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
(access_token, refresh_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a new access token with custom credentials
|
||||||
|
async fn get_custom_access_token(
|
||||||
|
mint_info: &MintInfo,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<(String, String), Error> {
|
||||||
|
let openid_discovery = mint_info
|
||||||
|
.nuts
|
||||||
|
.nut21
|
||||||
|
.clone()
|
||||||
|
.expect("Nutxx defined")
|
||||||
|
.openid_discovery;
|
||||||
|
|
||||||
|
let oidc_client = OidcClient::new(openid_discovery);
|
||||||
|
|
||||||
|
// Get the token endpoint from the OIDC configuration
|
||||||
|
let token_url = oidc_client
|
||||||
|
.get_oidc_config()
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Custom("Failed to get OIDC config".to_string()))?
|
||||||
|
.token_endpoint;
|
||||||
|
|
||||||
|
// Create the request parameters
|
||||||
|
let params = [
|
||||||
|
("grant_type", "password"),
|
||||||
|
("client_id", "cashu-client"),
|
||||||
|
("username", username),
|
||||||
|
("password", password),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make the token request directly
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.post(token_url)
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Custom("Failed to send token request".to_string()))?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(Error::Custom(format!(
|
||||||
|
"Token request failed with status: {}",
|
||||||
|
response.status()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_response: serde_json::Value = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Custom("Failed to parse token response".to_string()))?;
|
||||||
|
|
||||||
|
let access_token = token_response["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| Error::Custom("No access token in response".to_string()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let refresh_token = token_response["refresh_token"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| Error::Custom("No refresh token in response".to_string()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok((access_token, refresh_token))
|
||||||
|
}
|
||||||
@@ -8,8 +8,7 @@ use cdk::nuts::{
|
|||||||
CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, PreMintSecrets, Proofs,
|
CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, PreMintSecrets, Proofs,
|
||||||
SecretKey, State, SwapRequest,
|
SecretKey, State, SwapRequest,
|
||||||
};
|
};
|
||||||
use cdk::wallet::client::{HttpClient, MintConnector};
|
use cdk::wallet::{HttpClient, MintConnector, Wallet};
|
||||||
use cdk::wallet::Wallet;
|
|
||||||
use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
|
use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
|
||||||
use cdk_integration_tests::{attempt_to_swap_pending, wait_for_mint_to_be_paid};
|
use cdk_integration_tests::{attempt_to_swap_pending, wait_for_mint_to_be_paid};
|
||||||
use cdk_sqlite::wallet::memory;
|
use cdk_sqlite::wallet::memory;
|
||||||
@@ -354,7 +353,7 @@ async fn test_fake_melt_change_in_quote() -> Result<()> {
|
|||||||
|
|
||||||
let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default())?;
|
let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default())?;
|
||||||
|
|
||||||
let client = HttpClient::new(MINT_URL.parse()?);
|
let client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
|
|
||||||
let melt_request = MeltBolt11Request {
|
let melt_request = MeltBolt11Request {
|
||||||
quote: melt_quote.id.clone(),
|
quote: melt_quote.id.clone(),
|
||||||
@@ -450,7 +449,7 @@ async fn test_fake_mint_without_witness() -> Result<()> {
|
|||||||
|
|
||||||
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
|
|
||||||
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
||||||
|
|
||||||
@@ -486,7 +485,7 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> {
|
|||||||
|
|
||||||
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
|
wait_for_mint_to_be_paid(&wallet, &mint_quote.id, 60).await?;
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
|
|
||||||
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
||||||
|
|
||||||
@@ -545,7 +544,7 @@ async fn test_fake_mint_inflated() -> Result<()> {
|
|||||||
if let Some(secret_key) = quote_info.secret_key {
|
if let Some(secret_key) = quote_info.secret_key {
|
||||||
mint_request.sign(secret_key)?;
|
mint_request.sign(secret_key)?;
|
||||||
}
|
}
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
|
|
||||||
let response = http_client.post_mint(mint_request.clone()).await;
|
let response = http_client.post_mint(mint_request.clone()).await;
|
||||||
|
|
||||||
@@ -615,7 +614,7 @@ async fn test_fake_mint_multiple_units() -> Result<()> {
|
|||||||
if let Some(secret_key) = quote_info.secret_key {
|
if let Some(secret_key) = quote_info.secret_key {
|
||||||
mint_request.sign(secret_key)?;
|
mint_request.sign(secret_key)?;
|
||||||
}
|
}
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
|
|
||||||
let response = http_client.post_mint(mint_request.clone()).await;
|
let response = http_client.post_mint(mint_request.clone()).await;
|
||||||
|
|
||||||
@@ -682,7 +681,7 @@ async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -719,7 +718,7 @@ async fn test_fake_mint_multiple_unit_swap() -> Result<()> {
|
|||||||
outputs: usd_outputs,
|
outputs: usd_outputs,
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -793,7 +792,7 @@ async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
|
|||||||
outputs: None,
|
outputs: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_melt(melt_request.clone()).await;
|
let response = http_client.post_melt(melt_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -837,7 +836,7 @@ async fn test_fake_mint_multiple_unit_melt() -> Result<()> {
|
|||||||
outputs: Some(usd_outputs),
|
outputs: Some(usd_outputs),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
|
|
||||||
let response = http_client.post_melt(melt_request.clone()).await;
|
let response = http_client.post_melt(melt_request.clone()).await;
|
||||||
|
|
||||||
@@ -896,7 +895,7 @@ async fn test_fake_mint_input_output_mismatch() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -936,7 +935,7 @@ async fn test_fake_mint_swap_inflated() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -979,7 +978,7 @@ async fn test_fake_mint_swap_spend_after_fail() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
assert!(response.is_ok());
|
assert!(response.is_ok());
|
||||||
@@ -991,7 +990,7 @@ async fn test_fake_mint_swap_spend_after_fail() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -1009,7 +1008,7 @@ async fn test_fake_mint_swap_spend_after_fail() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -1052,7 +1051,7 @@ async fn test_fake_mint_melt_spend_after_fail() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
assert!(response.is_ok());
|
assert!(response.is_ok());
|
||||||
@@ -1064,7 +1063,7 @@ async fn test_fake_mint_melt_spend_after_fail() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -1085,7 +1084,7 @@ async fn test_fake_mint_melt_spend_after_fail() -> Result<()> {
|
|||||||
outputs: None,
|
outputs: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_melt(melt_request.clone()).await;
|
let response = http_client.post_melt(melt_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -1132,7 +1131,7 @@ async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> {
|
|||||||
outputs: pre_mint.blinded_messages(),
|
outputs: pre_mint.blinded_messages(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -1156,7 +1155,7 @@ async fn test_fake_mint_duplicate_proofs_swap() -> Result<()> {
|
|||||||
|
|
||||||
let swap_request = SwapRequest { inputs, outputs };
|
let swap_request = SwapRequest { inputs, outputs };
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_swap(swap_request.clone()).await;
|
let response = http_client.post_swap(swap_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
@@ -1206,7 +1205,7 @@ async fn test_fake_mint_duplicate_proofs_melt() -> Result<()> {
|
|||||||
outputs: None,
|
outputs: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let http_client = HttpClient::new(MINT_URL.parse()?);
|
let http_client = HttpClient::new(MINT_URL.parse()?, None);
|
||||||
let response = http_client.post_melt(melt_request.clone()).await;
|
let response = http_client.post_melt(melt_request.clone()).await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ use cdk::nuts::{
|
|||||||
CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload,
|
CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload,
|
||||||
PreMintSecrets, State,
|
PreMintSecrets, State,
|
||||||
};
|
};
|
||||||
use cdk::wallet::client::{HttpClient, MintConnector};
|
use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
|
||||||
use cdk::wallet::Wallet;
|
|
||||||
use cdk::WalletSubscription;
|
|
||||||
use cdk_integration_tests::init_regtest::{
|
use cdk_integration_tests::init_regtest::{
|
||||||
get_cln_dir, get_lnd_cert_file_path, get_lnd_dir, get_lnd_macaroon_path, get_mint_port,
|
get_cln_dir, get_lnd_cert_file_path, get_lnd_dir, get_lnd_macaroon_path, get_mint_port,
|
||||||
get_mint_url, get_mint_ws_url, LND_RPC_ADDR, LND_TWO_RPC_ADDR,
|
get_mint_url, get_mint_ws_url, LND_RPC_ADDR, LND_TWO_RPC_ADDR,
|
||||||
@@ -442,7 +440,7 @@ async fn test_cached_mint() -> Result<()> {
|
|||||||
wait_for_mint_to_be_paid(&wallet, "e.id, 60).await?;
|
wait_for_mint_to_be_paid(&wallet, "e.id, 60).await?;
|
||||||
|
|
||||||
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
let active_keyset_id = wallet.get_active_mint_keyset().await?.id;
|
||||||
let http_client = HttpClient::new(get_mint_url("0").as_str().parse()?);
|
let http_client = HttpClient::new(get_mint_url("0").as_str().parse()?, None);
|
||||||
let premint_secrets =
|
let premint_secrets =
|
||||||
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
|
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ description = "CDK mint binary"
|
|||||||
rust-version = "1.75.0"
|
rust-version = "1.75.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["management-rpc", "cln", "lnd", "lnbits", "fakewallet", "grpc-processor"]
|
default = ["management-rpc", "cln", "lnd", "lnbits", "fakewallet", "grpc-processor", "auth"]
|
||||||
# Ensure at least one lightning backend is enabled
|
# Ensure at least one lightning backend is enabled
|
||||||
management-rpc = ["cdk-mint-rpc"]
|
management-rpc = ["cdk-mint-rpc"]
|
||||||
cln = ["dep:cdk-cln"]
|
cln = ["dep:cdk-cln"]
|
||||||
@@ -23,6 +23,7 @@ sqlcipher = ["cdk-sqlite/sqlcipher"]
|
|||||||
redb = ["dep:cdk-redb"]
|
redb = ["dep:cdk-redb"]
|
||||||
swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
|
swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
|
||||||
redis = ["cdk-axum/redis"]
|
redis = ["cdk-axum/redis"]
|
||||||
|
auth = ["cdk/auth", "cdk-sqlite/auth"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
@@ -33,6 +34,7 @@ cdk = { workspace = true, features = [
|
|||||||
] }
|
] }
|
||||||
cdk-redb = { workspace = true, features = [
|
cdk-redb = { workspace = true, features = [
|
||||||
"mint",
|
"mint",
|
||||||
|
"auth"
|
||||||
], optional = true }
|
], optional = true }
|
||||||
cdk-sqlite = { workspace = true, features = [
|
cdk-sqlite = { workspace = true, features = [
|
||||||
"mint",
|
"mint",
|
||||||
|
|||||||
@@ -98,3 +98,14 @@ reserve_fee_min = 4
|
|||||||
# addr = "127.0.0.1"
|
# addr = "127.0.0.1"
|
||||||
# port = 50051
|
# port = 50051
|
||||||
# tls_dir = "/path/to/tls"
|
# tls_dir = "/path/to/tls"
|
||||||
|
|
||||||
|
# [auth]
|
||||||
|
# openid_discovery = "http://127.0.0.1:8080/realms/cdk-test-realm/.well-known/openid-configuration"
|
||||||
|
# openid_client_id = "cashu-client"
|
||||||
|
# mint_max_bat=50
|
||||||
|
# enabled_mint=true
|
||||||
|
# enabled_melt=true
|
||||||
|
# enabled_swap=true
|
||||||
|
# enabled_check_mint_quote=true
|
||||||
|
# enabled_check_melt_quote=true
|
||||||
|
# enabled_restore=true
|
||||||
|
|||||||
@@ -202,6 +202,30 @@ pub struct Database {
|
|||||||
pub engine: DatabaseEngine,
|
pub engine: DatabaseEngine,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Auth {
|
||||||
|
pub openid_discovery: String,
|
||||||
|
pub openid_client_id: String,
|
||||||
|
pub mint_max_bat: u64,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled_mint: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled_melt: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled_swap: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled_check_mint_quote: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled_check_melt_quote: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled_restore: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled_check_proof_state: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
/// CDK settings, derived from `config.toml`
|
/// CDK settings, derived from `config.toml`
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
@@ -220,6 +244,7 @@ pub struct Settings {
|
|||||||
pub database: Database,
|
pub database: Database,
|
||||||
#[cfg(feature = "management-rpc")]
|
#[cfg(feature = "management-rpc")]
|
||||||
pub mint_management_rpc: Option<MintManagementRpc>,
|
pub mint_management_rpc: Option<MintManagementRpc>,
|
||||||
|
pub auth: Option<Auth>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
|||||||
78
crates/cdk-mintd/src/env_vars/auth.rs
Normal file
78
crates/cdk-mintd/src/env_vars/auth.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
//! Auth env
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use crate::config::Auth;
|
||||||
|
|
||||||
|
pub const ENV_AUTH_OPENID_DISCOVERY: &str = "CDK_MINTD_AUTH_OPENID_DISCOVERY";
|
||||||
|
pub const ENV_AUTH_OPENID_CLIENT_ID: &str = "CDK_MINTD_AUTH_OPENID_CLIENT_ID";
|
||||||
|
pub const ENV_AUTH_MINT_MAX_BAT: &str = "CDK_MINTD_AUTH_MINT_MAX_BAT";
|
||||||
|
pub const ENV_AUTH_ENABLED_MINT: &str = "CDK_MINTD_AUTH_ENABLED_MINT";
|
||||||
|
pub const ENV_AUTH_ENABLED_MELT: &str = "CDK_MINTD_AUTH_ENABLED_MELT";
|
||||||
|
pub const ENV_AUTH_ENABLED_SWAP: &str = "CDK_MINTD_AUTH_ENABLED_SWAP";
|
||||||
|
pub const ENV_AUTH_ENABLED_CHECK_MINT_QUOTE: &str = "CDK_MINTD_AUTH_ENABLED_CHECK_MINT_QUOTE";
|
||||||
|
pub const ENV_AUTH_ENABLED_CHECK_MELT_QUOTE: &str = "CDK_MINTD_AUTH_ENABLED_CHECK_MELT_QUOTE";
|
||||||
|
pub const ENV_AUTH_ENABLED_RESTORE: &str = "CDK_MINTD_AUTH_ENABLED_RESTORE";
|
||||||
|
pub const ENV_AUTH_ENABLED_CHECK_PROOF_STATE: &str = "CDK_MINTD_AUTH_ENABLED_CHECK_PROOF_STATE";
|
||||||
|
|
||||||
|
impl Auth {
|
||||||
|
pub fn from_env(mut self) -> Self {
|
||||||
|
if let Ok(discovery) = env::var(ENV_AUTH_OPENID_DISCOVERY) {
|
||||||
|
self.openid_discovery = discovery;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(client_id) = env::var(ENV_AUTH_OPENID_CLIENT_ID) {
|
||||||
|
self.openid_client_id = client_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(max_bat_str) = env::var(ENV_AUTH_MINT_MAX_BAT) {
|
||||||
|
if let Ok(max_bat) = max_bat_str.parse() {
|
||||||
|
self.mint_max_bat = max_bat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(enabled_mint_str) = env::var(ENV_AUTH_ENABLED_MINT) {
|
||||||
|
if let Ok(enabled) = enabled_mint_str.parse() {
|
||||||
|
self.enabled_mint = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(enabled_melt_str) = env::var(ENV_AUTH_ENABLED_MELT) {
|
||||||
|
if let Ok(enabled) = enabled_melt_str.parse() {
|
||||||
|
self.enabled_melt = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(enabled_swap_str) = env::var(ENV_AUTH_ENABLED_SWAP) {
|
||||||
|
if let Ok(enabled) = enabled_swap_str.parse() {
|
||||||
|
self.enabled_swap = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(enabled_check_mint_str) = env::var(ENV_AUTH_ENABLED_CHECK_MINT_QUOTE) {
|
||||||
|
if let Ok(enabled) = enabled_check_mint_str.parse() {
|
||||||
|
self.enabled_check_mint_quote = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(enabled_check_melt_str) = env::var(ENV_AUTH_ENABLED_CHECK_MELT_QUOTE) {
|
||||||
|
if let Ok(enabled) = enabled_check_melt_str.parse() {
|
||||||
|
self.enabled_check_melt_quote = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(enabled_restore_str) = env::var(ENV_AUTH_ENABLED_RESTORE) {
|
||||||
|
if let Ok(enabled) = enabled_restore_str.parse() {
|
||||||
|
self.enabled_restore = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(enabled_check_proof_str) = env::var(ENV_AUTH_ENABLED_CHECK_PROOF_STATE) {
|
||||||
|
if let Ok(enabled) = enabled_check_proof_str.parse() {
|
||||||
|
self.enabled_check_proof_state = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ mod info;
|
|||||||
mod ln;
|
mod ln;
|
||||||
mod mint_info;
|
mod mint_info;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
#[cfg(feature = "cln")]
|
#[cfg(feature = "cln")]
|
||||||
mod cln;
|
mod cln;
|
||||||
#[cfg(feature = "fakewallet")]
|
#[cfg(feature = "fakewallet")]
|
||||||
@@ -25,6 +27,8 @@ use std::env;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use auth::*;
|
||||||
#[cfg(feature = "cln")]
|
#[cfg(feature = "cln")]
|
||||||
pub use cln::*;
|
pub use cln::*;
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
@@ -54,6 +58,25 @@ impl Settings {
|
|||||||
self.mint_info = self.mint_info.clone().from_env();
|
self.mint_info = self.mint_info.clone().from_env();
|
||||||
self.ln = self.ln.clone().from_env();
|
self.ln = self.ln.clone().from_env();
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
// Check env vars for auth config even if None
|
||||||
|
let auth = self.auth.clone().unwrap_or_default().from_env();
|
||||||
|
|
||||||
|
// Only set auth if env vars are present and have non-default values
|
||||||
|
if auth.openid_discovery != String::default()
|
||||||
|
|| auth.openid_client_id != String::default()
|
||||||
|
|| auth.mint_max_bat != 0
|
||||||
|
|| auth.enabled_mint
|
||||||
|
|| auth.enabled_melt
|
||||||
|
|| auth.enabled_swap
|
||||||
|
{
|
||||||
|
self.auth = Some(auth);
|
||||||
|
} else {
|
||||||
|
self.auth = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "management-rpc")]
|
#[cfg(feature = "management-rpc")]
|
||||||
{
|
{
|
||||||
self.mint_management_rpc = Some(
|
self.mint_management_rpc = Some(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! CDK Mint Server
|
//! CDK MINTD
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![warn(rustdoc::bare_urls)]
|
#![warn(rustdoc::bare_urls)]
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -12,7 +12,7 @@ use std::sync::Arc;
|
|||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use bip39::Mnemonic;
|
use bip39::Mnemonic;
|
||||||
use cdk::cdk_database::{self, MintDatabase};
|
use cdk::cdk_database::{self, MintAuthDatabase, MintDatabase};
|
||||||
use cdk::mint::{MintBuilder, MintMeltLimits};
|
use cdk::mint::{MintBuilder, MintMeltLimits};
|
||||||
// Feature-gated imports
|
// Feature-gated imports
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
@@ -31,7 +31,9 @@ use cdk::nuts::nut19::{CachedEndpoint, Method as NUT19Method, Path as NUT19Path}
|
|||||||
feature = "fakewallet"
|
feature = "fakewallet"
|
||||||
))]
|
))]
|
||||||
use cdk::nuts::CurrencyUnit;
|
use cdk::nuts::CurrencyUnit;
|
||||||
use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod};
|
use cdk::nuts::{
|
||||||
|
AuthRequired, ContactInfo, MintVersion, PaymentMethod, ProtectedEndpoint, RoutePath,
|
||||||
|
};
|
||||||
use cdk::types::QuoteTTL;
|
use cdk::types::QuoteTTL;
|
||||||
use cdk_axum::cache::HttpCache;
|
use cdk_axum::cache::HttpCache;
|
||||||
#[cfg(feature = "management-rpc")]
|
#[cfg(feature = "management-rpc")]
|
||||||
@@ -41,7 +43,10 @@ use cdk_mintd::config::{self, DatabaseEngine, LnBackend};
|
|||||||
use cdk_mintd::env_vars::ENV_WORK_DIR;
|
use cdk_mintd::env_vars::ENV_WORK_DIR;
|
||||||
use cdk_mintd::setup::LnBackendSetup;
|
use cdk_mintd::setup::LnBackendSetup;
|
||||||
#[cfg(feature = "redb")]
|
#[cfg(feature = "redb")]
|
||||||
|
use cdk_redb::mint::MintRedbAuthDatabase;
|
||||||
|
#[cfg(feature = "redb")]
|
||||||
use cdk_redb::MintRedbDatabase;
|
use cdk_redb::MintRedbDatabase;
|
||||||
|
use cdk_sqlite::mint::MintSqliteAuthDatabase;
|
||||||
use cdk_sqlite::MintSqliteDatabase;
|
use cdk_sqlite::MintSqliteDatabase;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
@@ -361,6 +366,157 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
mint_builder = mint_builder.add_cache(Some(cache.ttl.as_secs()), cached_endpoints);
|
mint_builder = mint_builder.add_cache(Some(cache.ttl.as_secs()), cached_endpoints);
|
||||||
|
|
||||||
|
// Add auth to mint
|
||||||
|
if let Some(auth_settings) = settings.auth {
|
||||||
|
tracing::info!("Auth settings are defined. {:?}", auth_settings);
|
||||||
|
let auth_localstore: Arc<dyn MintAuthDatabase<Err = cdk_database::Error> + 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)
|
||||||
|
}
|
||||||
|
#[cfg(feature = "redb")]
|
||||||
|
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.clone());
|
||||||
|
|
||||||
|
let mint_blind_auth_endpoint =
|
||||||
|
ProtectedEndpoint::new(cdk::nuts::Method::Post, RoutePath::MintBlindAuth);
|
||||||
|
|
||||||
|
mint_builder = mint_builder.set_clear_auth_settings(
|
||||||
|
auth_settings.openid_discovery,
|
||||||
|
auth_settings.openid_client_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut protected_endpoints = HashMap::new();
|
||||||
|
|
||||||
|
protected_endpoints.insert(mint_blind_auth_endpoint, AuthRequired::Clear);
|
||||||
|
|
||||||
|
let mut blind_auth_endpoints = vec![];
|
||||||
|
let mut unprotected_endpoints = vec![];
|
||||||
|
|
||||||
|
{
|
||||||
|
let mint_quote_protected_endpoint = ProtectedEndpoint::new(
|
||||||
|
cdk::nuts::Method::Post,
|
||||||
|
cdk::nuts::RoutePath::MintQuoteBolt11,
|
||||||
|
);
|
||||||
|
let mint_protected_endpoint =
|
||||||
|
ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::MintBolt11);
|
||||||
|
if auth_settings.enabled_mint {
|
||||||
|
protected_endpoints.insert(mint_quote_protected_endpoint, AuthRequired::Blind);
|
||||||
|
|
||||||
|
protected_endpoints.insert(mint_protected_endpoint, AuthRequired::Blind);
|
||||||
|
|
||||||
|
blind_auth_endpoints.push(mint_quote_protected_endpoint);
|
||||||
|
blind_auth_endpoints.push(mint_protected_endpoint);
|
||||||
|
} else {
|
||||||
|
unprotected_endpoints.push(mint_protected_endpoint);
|
||||||
|
unprotected_endpoints.push(mint_quote_protected_endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let melt_quote_protected_endpoint = ProtectedEndpoint::new(
|
||||||
|
cdk::nuts::Method::Post,
|
||||||
|
cdk::nuts::RoutePath::MeltQuoteBolt11,
|
||||||
|
);
|
||||||
|
let melt_protected_endpoint =
|
||||||
|
ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::MeltBolt11);
|
||||||
|
|
||||||
|
if auth_settings.enabled_melt {
|
||||||
|
protected_endpoints.insert(melt_quote_protected_endpoint, AuthRequired::Blind);
|
||||||
|
protected_endpoints.insert(melt_protected_endpoint, AuthRequired::Blind);
|
||||||
|
|
||||||
|
blind_auth_endpoints.push(melt_quote_protected_endpoint);
|
||||||
|
blind_auth_endpoints.push(melt_protected_endpoint);
|
||||||
|
} else {
|
||||||
|
unprotected_endpoints.push(melt_quote_protected_endpoint);
|
||||||
|
unprotected_endpoints.push(melt_protected_endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let swap_protected_endpoint =
|
||||||
|
ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::Swap);
|
||||||
|
|
||||||
|
if auth_settings.enabled_swap {
|
||||||
|
protected_endpoints.insert(swap_protected_endpoint, AuthRequired::Blind);
|
||||||
|
blind_auth_endpoints.push(swap_protected_endpoint);
|
||||||
|
} else {
|
||||||
|
unprotected_endpoints.push(swap_protected_endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let check_mint_protected_endpoint = ProtectedEndpoint::new(
|
||||||
|
cdk::nuts::Method::Get,
|
||||||
|
cdk::nuts::RoutePath::MintQuoteBolt11,
|
||||||
|
);
|
||||||
|
|
||||||
|
if auth_settings.enabled_check_mint_quote {
|
||||||
|
protected_endpoints.insert(check_mint_protected_endpoint, AuthRequired::Blind);
|
||||||
|
blind_auth_endpoints.push(check_mint_protected_endpoint);
|
||||||
|
} else {
|
||||||
|
unprotected_endpoints.push(check_mint_protected_endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let check_melt_protected_endpoint = ProtectedEndpoint::new(
|
||||||
|
cdk::nuts::Method::Get,
|
||||||
|
cdk::nuts::RoutePath::MeltQuoteBolt11,
|
||||||
|
);
|
||||||
|
|
||||||
|
if auth_settings.enabled_check_melt_quote {
|
||||||
|
protected_endpoints.insert(check_melt_protected_endpoint, AuthRequired::Blind);
|
||||||
|
blind_auth_endpoints.push(check_melt_protected_endpoint);
|
||||||
|
} else {
|
||||||
|
unprotected_endpoints.push(check_melt_protected_endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let restore_protected_endpoint =
|
||||||
|
ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::Restore);
|
||||||
|
|
||||||
|
if auth_settings.enabled_restore {
|
||||||
|
protected_endpoints.insert(restore_protected_endpoint, AuthRequired::Blind);
|
||||||
|
blind_auth_endpoints.push(restore_protected_endpoint);
|
||||||
|
} else {
|
||||||
|
unprotected_endpoints.push(restore_protected_endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let state_protected_endpoint =
|
||||||
|
ProtectedEndpoint::new(cdk::nuts::Method::Post, cdk::nuts::RoutePath::Checkstate);
|
||||||
|
|
||||||
|
if auth_settings.enabled_check_proof_state {
|
||||||
|
protected_endpoints.insert(state_protected_endpoint, AuthRequired::Blind);
|
||||||
|
blind_auth_endpoints.push(state_protected_endpoint);
|
||||||
|
} else {
|
||||||
|
unprotected_endpoints.push(state_protected_endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mint_builder = mint_builder.set_blind_auth_settings(auth_settings.mint_max_bat);
|
||||||
|
|
||||||
|
auth_localstore
|
||||||
|
.remove_protected_endpoints(unprotected_endpoints)
|
||||||
|
.await?;
|
||||||
|
auth_localstore
|
||||||
|
.add_protected_endpoints(protected_endpoints)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let mint = mint_builder.build().await?;
|
let mint = mint_builder.build().await?;
|
||||||
|
|
||||||
tracing::debug!("Mint built from builder.");
|
tracing::debug!("Mint built from builder.");
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ rust-version = "1.81.0" # MSRV
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[features]
|
[features]
|
||||||
default = ["mint", "wallet"]
|
default = ["mint", "wallet", "auth"]
|
||||||
mint = ["cdk-common/mint"]
|
mint = ["cdk-common/mint"]
|
||||||
wallet = ["cdk-common/wallet"]
|
wallet = ["cdk-common/wallet"]
|
||||||
|
auth = ["cdk-common/auth"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
|||||||
391
crates/cdk-redb/src/mint/auth/mod.rs
Normal file
391
crates/cdk-redb/src/mint/auth/mod.rs
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use cdk_common::database::{self, MintAuthDatabase};
|
||||||
|
use cdk_common::dhke::hash_to_curve;
|
||||||
|
use cdk_common::mint::MintKeySetInfo;
|
||||||
|
use cdk_common::nuts::{AuthProof, BlindSignature, Id, PublicKey, State};
|
||||||
|
use cdk_common::{AuthRequired, ProtectedEndpoint};
|
||||||
|
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");
|
||||||
|
const ENDPOINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("endpoints");
|
||||||
|
|
||||||
|
/// Mint Redbdatabase
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MintRedbAuthDatabase {
|
||||||
|
db: Arc<Database>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATABASE_VERSION: u32 = 0;
|
||||||
|
|
||||||
|
impl MintRedbAuthDatabase {
|
||||||
|
/// Create new [`MintRedbDatabase`]
|
||||||
|
pub fn new(path: &Path) -> Result<Self, Error> {
|
||||||
|
{
|
||||||
|
// 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<Option<Id>, 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<Option<MintKeySetInfo>, 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<Vec<MintKeySetInfo>, 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<Option<State>, 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<Vec<Option<State>>, 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<Vec<Option<BlindSignature>>, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_protected_endpoints(
|
||||||
|
&self,
|
||||||
|
protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut table = write_txn.open_table(ENDPOINTS_TABLE).map_err(Error::from)?;
|
||||||
|
for (endpoint, auth) in protected_endpoints.iter() {
|
||||||
|
table
|
||||||
|
.insert(
|
||||||
|
serde_json::to_string(endpoint)
|
||||||
|
.map_err(Error::from)?
|
||||||
|
.as_str(),
|
||||||
|
serde_json::to_string(&auth).map_err(Error::from)?.as_str(),
|
||||||
|
)
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_txn.commit().map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_protected_endpoints(
|
||||||
|
&self,
|
||||||
|
protected_endpoints: Vec<ProtectedEndpoint>,
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
let write_txn = self.db.begin_write().map_err(Error::from)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut table = write_txn.open_table(ENDPOINTS_TABLE).map_err(Error::from)?;
|
||||||
|
for endpoint in protected_endpoints.iter() {
|
||||||
|
table
|
||||||
|
.remove(
|
||||||
|
serde_json::to_string(endpoint)
|
||||||
|
.map_err(Error::from)?
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_txn.commit().map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_auth_for_endpoint(
|
||||||
|
&self,
|
||||||
|
protected_endpoint: ProtectedEndpoint,
|
||||||
|
) -> Result<Option<AuthRequired>, Self::Err> {
|
||||||
|
let read_txn = self.db.begin_read().map_err(Error::from)?;
|
||||||
|
let table = read_txn.open_table(ENDPOINTS_TABLE).map_err(Error::from)?;
|
||||||
|
|
||||||
|
match table
|
||||||
|
.get(
|
||||||
|
serde_json::to_string(&protected_endpoint)
|
||||||
|
.map_err(Error::from)?
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
.map_err(Error::from)?
|
||||||
|
{
|
||||||
|
Some(auth) => Ok(serde_json::from_str(auth.value()).map_err(Error::from)?),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get_auth_for_endpoints(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<ProtectedEndpoint, Option<AuthRequired>>, Self::Err> {
|
||||||
|
let read_txn = self.db.begin_read().map_err(Error::from)?;
|
||||||
|
let table = read_txn.open_table(ENDPOINTS_TABLE).map_err(Error::from)?;
|
||||||
|
|
||||||
|
let mut protected = HashMap::new();
|
||||||
|
|
||||||
|
for (endpoint, auth) in (table.iter().map_err(Error::from)?).flatten() {
|
||||||
|
let endpoint: ProtectedEndpoint =
|
||||||
|
serde_json::from_str(endpoint.value()).map_err(Error::from)?;
|
||||||
|
let auth: AuthRequired = serde_json::from_str(auth.value()).map_err(Error::from)?;
|
||||||
|
|
||||||
|
protected.insert(endpoint, Some(auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(protected)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,8 +24,13 @@ use super::error::Error;
|
|||||||
use crate::migrations::migrate_00_to_01;
|
use crate::migrations::migrate_00_to_01;
|
||||||
use crate::mint::migrations::{migrate_02_to_03, migrate_03_to_04};
|
use crate::mint::migrations::{migrate_02_to_03, migrate_03_to_04};
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
mod migrations;
|
mod migrations;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use auth::MintRedbAuthDatabase;
|
||||||
|
|
||||||
const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets");
|
const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets");
|
||||||
const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets");
|
const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets");
|
||||||
const MINT_QUOTES_TABLE: TableDefinition<[u8; 16], &str> = TableDefinition::new("mint_quotes");
|
const MINT_QUOTES_TABLE: TableDefinition<[u8; 16], &str> = TableDefinition::new("mint_quotes");
|
||||||
@@ -133,8 +138,8 @@ impl MintRedbDatabase {
|
|||||||
None => {
|
None => {
|
||||||
let write_txn = db.begin_write()?;
|
let write_txn = db.begin_write()?;
|
||||||
{
|
{
|
||||||
let mut table = write_txn.open_table(CONFIG_TABLE)?;
|
|
||||||
// Open all tables to init a new db
|
// 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(ACTIVE_KEYSETS_TABLE)?;
|
||||||
let _ = write_txn.open_table(KEYSETS_TABLE)?;
|
let _ = write_txn.open_table(KEYSETS_TABLE)?;
|
||||||
let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
|
let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
|
||||||
@@ -965,7 +970,7 @@ mod tests {
|
|||||||
let proofs = vec![
|
let proofs = vec![
|
||||||
Proof {
|
Proof {
|
||||||
amount: Amount::from(100),
|
amount: Amount::from(100),
|
||||||
keyset_id: keyset_id.clone(),
|
keyset_id,
|
||||||
secret: Secret::generate(),
|
secret: Secret::generate(),
|
||||||
c: SecretKey::generate().public_key(),
|
c: SecretKey::generate().public_key(),
|
||||||
witness: None,
|
witness: None,
|
||||||
@@ -973,7 +978,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
Proof {
|
Proof {
|
||||||
amount: Amount::from(200),
|
amount: Amount::from(200),
|
||||||
keyset_id: keyset_id.clone(),
|
keyset_id,
|
||||||
secret: Secret::generate(),
|
secret: Secret::generate(),
|
||||||
c: SecretKey::generate().public_key(),
|
c: SecretKey::generate().public_key(),
|
||||||
witness: None,
|
witness: None,
|
||||||
@@ -1026,7 +1031,7 @@ mod tests {
|
|||||||
let proofs = vec![
|
let proofs = vec![
|
||||||
Proof {
|
Proof {
|
||||||
amount: Amount::from(100),
|
amount: Amount::from(100),
|
||||||
keyset_id: keyset_id.clone(),
|
keyset_id,
|
||||||
secret: Secret::generate(),
|
secret: Secret::generate(),
|
||||||
c: SecretKey::generate().public_key(),
|
c: SecretKey::generate().public_key(),
|
||||||
witness: None,
|
witness: None,
|
||||||
@@ -1034,7 +1039,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
Proof {
|
Proof {
|
||||||
amount: Amount::from(200),
|
amount: Amount::from(200),
|
||||||
keyset_id: keyset_id.clone(),
|
keyset_id,
|
||||||
secret: Secret::generate(),
|
secret: Secret::generate(),
|
||||||
c: SecretKey::generate().public_key(),
|
c: SecretKey::generate().public_key(),
|
||||||
witness: None,
|
witness: None,
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ rust-version = "1.75.0" # MSRV
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[features]
|
[features]
|
||||||
default = ["mint", "wallet"]
|
default = ["mint", "wallet", "auth"]
|
||||||
mint = ["cdk-common/mint"]
|
mint = ["cdk-common/mint"]
|
||||||
wallet = ["cdk-common/wallet"]
|
wallet = ["cdk-common/wallet"]
|
||||||
|
auth = ["cdk-common/auth"]
|
||||||
sqlcipher = ["libsqlite3-sys"]
|
sqlcipher = ["libsqlite3-sys"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS protected_endpoints (
|
||||||
|
endpoint TEXT PRIMARY KEY,
|
||||||
|
auth TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
539
crates/cdk-sqlite/src/mint/auth/mod.rs
Normal file
539
crates/cdk-sqlite/src/mint/auth/mod.rs
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
//! 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 cdk_common::database::{self, MintAuthDatabase};
|
||||||
|
use cdk_common::mint::MintKeySetInfo;
|
||||||
|
use cdk_common::nuts::{AuthProof, BlindSignature, Id, PublicKey, State};
|
||||||
|
use cdk_common::{AuthRequired, ProtectedEndpoint};
|
||||||
|
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||||
|
use sqlx::Row;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
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<Self, Error> {
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
async fn set_active_keyset(&self, id: Id) -> Result<(), Self::Err> {
|
||||||
|
tracing::info!("Setting auth keyset {id} active");
|
||||||
|
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<Option<Id>, 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<Option<MintKeySetInfo>, 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<Vec<MintKeySetInfo>, 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::<Result<_, _>>()?)
|
||||||
|
}
|
||||||
|
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<Vec<Option<State>>, 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::<Result<HashMap<_, _>, _>>()?;
|
||||||
|
|
||||||
|
Ok(ys.iter().map(|y| current_states.remove(y)).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_proof_state(
|
||||||
|
&self,
|
||||||
|
y: &PublicKey,
|
||||||
|
proofs_state: State,
|
||||||
|
) -> Result<Option<State>, 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<Vec<Option<BlindSignature>>, 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::<Result<HashMap<_, _>, _>>()?;
|
||||||
|
|
||||||
|
Ok(blinded_messages
|
||||||
|
.iter()
|
||||||
|
.map(|y| blinded_signatures.remove(y))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_protected_endpoints(
|
||||||
|
&self,
|
||||||
|
protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
for (endpoint, auth) in protected_endpoints.iter() {
|
||||||
|
if let Err(err) = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT OR REPLACE INTO protected_endpoints
|
||||||
|
(endpoint, auth)
|
||||||
|
VALUES (?, ?);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(serde_json::to_string(endpoint)?)
|
||||||
|
.bind(serde_json::to_string(auth)?)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)
|
||||||
|
{
|
||||||
|
tracing::debug!(
|
||||||
|
"Attempting to add protected endpoint. Skipping.... {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn remove_protected_endpoints(
|
||||||
|
&self,
|
||||||
|
protected_endpoints: Vec<ProtectedEndpoint>,
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let sql = format!(
|
||||||
|
"DELETE FROM protected_endpoints WHERE endpoint IN ({})",
|
||||||
|
std::iter::repeat("?")
|
||||||
|
.take(protected_endpoints.len())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
);
|
||||||
|
|
||||||
|
let endpoints = protected_endpoints
|
||||||
|
.iter()
|
||||||
|
.map(serde_json::to_string)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
endpoints
|
||||||
|
.iter()
|
||||||
|
.fold(sqlx::query(&sql), |query, endpoint| query.bind(endpoint))
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)?;
|
||||||
|
|
||||||
|
transaction.commit().await.map_err(Error::from)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn get_auth_for_endpoint(
|
||||||
|
&self,
|
||||||
|
protected_endpoint: ProtectedEndpoint,
|
||||||
|
) -> Result<Option<AuthRequired>, Self::Err> {
|
||||||
|
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let rec = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM protected_endpoints
|
||||||
|
WHERE endpoint=?;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(serde_json::to_string(&protected_endpoint)?)
|
||||||
|
.fetch_one(&mut *transaction)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match rec {
|
||||||
|
Ok(rec) => {
|
||||||
|
transaction.commit().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let auth: String = rec.try_get("auth").map_err(Error::from)?;
|
||||||
|
|
||||||
|
Ok(Some(serde_json::from_str(&auth)?))
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get_auth_for_endpoints(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<ProtectedEndpoint, Option<AuthRequired>>, Self::Err> {
|
||||||
|
let mut transaction = self.pool.begin().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let recs = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT *
|
||||||
|
FROM protected_endpoints
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *transaction)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match recs {
|
||||||
|
Ok(recs) => {
|
||||||
|
transaction.commit().await.map_err(Error::from)?;
|
||||||
|
|
||||||
|
let mut endpoints = HashMap::new();
|
||||||
|
|
||||||
|
for rec in recs {
|
||||||
|
let auth: String = rec.try_get("auth").map_err(Error::from)?;
|
||||||
|
let endpoint: String = rec.try_get("endpoint").map_err(Error::from)?;
|
||||||
|
|
||||||
|
let endpoint: ProtectedEndpoint = serde_json::from_str(&endpoint)?;
|
||||||
|
let auth: AuthRequired = serde_json::from_str(&auth)?;
|
||||||
|
|
||||||
|
endpoints.insert(endpoint, Some(auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(endpoints)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("SQLite could not get protected endpoints");
|
||||||
|
if let Err(err) = transaction.rollback().await {
|
||||||
|
tracing::error!("Could not rollback sql transaction: {}", err);
|
||||||
|
}
|
||||||
|
Err(Error::from(err).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,9 +26,14 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::common::create_sqlite_pool;
|
use crate::common::create_sqlite_pool;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use auth::MintSqliteAuthDatabase;
|
||||||
|
|
||||||
/// Mint SQLite Database
|
/// Mint SQLite Database
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MintSqliteDatabase {
|
pub struct MintSqliteDatabase {
|
||||||
@@ -1565,7 +1570,7 @@ fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
|
|||||||
let row_valid_to: Option<i64> = row.try_get("valid_to").map_err(Error::from)?;
|
let row_valid_to: Option<i64> = 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_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_max_order: u8 = row.try_get("max_order").map_err(Error::from)?;
|
||||||
let row_keyset_ppk: Option<i64> = row.try_get("input_fee_ppk").map_err(Error::from)?;
|
let row_keyset_ppk: Option<i64> = row.try_get("input_fee_ppk").ok();
|
||||||
let row_derivation_path_index: Option<i64> =
|
let row_derivation_path_index: Option<i64> =
|
||||||
row.try_get("derivation_path_index").map_err(Error::from)?;
|
row.try_get("derivation_path_index").map_err(Error::from)?;
|
||||||
|
|
||||||
@@ -1748,7 +1753,7 @@ mod tests {
|
|||||||
// Create a keyset and add it to the database
|
// Create a keyset and add it to the database
|
||||||
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
|
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
|
||||||
let keyset_info = MintKeySetInfo {
|
let keyset_info = MintKeySetInfo {
|
||||||
id: keyset_id.clone(),
|
id: keyset_id,
|
||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
active: true,
|
active: true,
|
||||||
valid_from: 0,
|
valid_from: 0,
|
||||||
@@ -1763,7 +1768,7 @@ mod tests {
|
|||||||
let proofs = vec![
|
let proofs = vec![
|
||||||
Proof {
|
Proof {
|
||||||
amount: Amount::from(100),
|
amount: Amount::from(100),
|
||||||
keyset_id: keyset_id.clone(),
|
keyset_id,
|
||||||
secret: Secret::generate(),
|
secret: Secret::generate(),
|
||||||
c: SecretKey::generate().public_key(),
|
c: SecretKey::generate().public_key(),
|
||||||
witness: None,
|
witness: None,
|
||||||
@@ -1771,7 +1776,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
Proof {
|
Proof {
|
||||||
amount: Amount::from(200),
|
amount: Amount::from(200),
|
||||||
keyset_id: keyset_id.clone(),
|
keyset_id,
|
||||||
secret: Secret::generate(),
|
secret: Secret::generate(),
|
||||||
c: SecretKey::generate().public_key(),
|
c: SecretKey::generate().public_key(),
|
||||||
witness: None,
|
witness: None,
|
||||||
@@ -1816,7 +1821,7 @@ mod tests {
|
|||||||
// Create a keyset and add it to the database
|
// Create a keyset and add it to the database
|
||||||
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
|
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
|
||||||
let keyset_info = MintKeySetInfo {
|
let keyset_info = MintKeySetInfo {
|
||||||
id: keyset_id.clone(),
|
id: keyset_id,
|
||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
active: true,
|
active: true,
|
||||||
valid_from: 0,
|
valid_from: 0,
|
||||||
@@ -1831,7 +1836,7 @@ mod tests {
|
|||||||
let proofs = vec![
|
let proofs = vec![
|
||||||
Proof {
|
Proof {
|
||||||
amount: Amount::from(100),
|
amount: Amount::from(100),
|
||||||
keyset_id: keyset_id.clone(),
|
keyset_id,
|
||||||
secret: Secret::generate(),
|
secret: Secret::generate(),
|
||||||
c: SecretKey::generate().public_key(),
|
c: SecretKey::generate().public_key(),
|
||||||
witness: None,
|
witness: None,
|
||||||
@@ -1839,7 +1844,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
Proof {
|
Proof {
|
||||||
amount: Amount::from(200),
|
amount: Amount::from(200),
|
||||||
keyset_id: keyset_id.clone(),
|
keyset_id,
|
||||||
secret: Secret::generate(),
|
secret: Secret::generate(),
|
||||||
c: SecretKey::generate().public_key(),
|
c: SecretKey::generate().public_key(),
|
||||||
witness: None,
|
witness: None,
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ license = "MIT"
|
|||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["mint", "wallet"]
|
default = ["mint", "wallet", "auth"]
|
||||||
mint = ["dep:futures", "cdk-common/mint"]
|
wallet = ["dep:reqwest", "cdk-common/wallet"]
|
||||||
|
mint = ["dep:futures", "dep:reqwest", "cdk-common/mint"]
|
||||||
|
auth = ["dep:jsonwebtoken", "cdk-common/auth", "cdk-common/auth"]
|
||||||
# We do not commit to a MSRV with swagger enabled
|
# We do not commit to a MSRV with swagger enabled
|
||||||
swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
|
swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
|
||||||
wallet = ["dep:reqwest", "cdk-common/wallet"]
|
|
||||||
bench = []
|
bench = []
|
||||||
http_subscription = []
|
http_subscription = []
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ anyhow.workspace = true
|
|||||||
bitcoin.workspace = true
|
bitcoin.workspace = true
|
||||||
ciborium.workspace = true
|
ciborium.workspace = true
|
||||||
lightning-invoice.workspace = true
|
lightning-invoice.workspace = true
|
||||||
regex = "1"
|
regex.workspace = true
|
||||||
reqwest = { workspace = true, optional = true }
|
reqwest = { workspace = true, optional = true }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
@@ -39,6 +40,7 @@ futures = { workspace = true, optional = true, features = ["alloc"] }
|
|||||||
url.workspace = true
|
url.workspace = true
|
||||||
utoipa = { workspace = true, optional = true }
|
utoipa = { workspace = true, optional = true }
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
jsonwebtoken = { version = "9", optional = true }
|
||||||
|
|
||||||
# -Z minimal-versions
|
# -Z minimal-versions
|
||||||
sync_wrapper = "0.1.2"
|
sync_wrapper = "0.1.2"
|
||||||
@@ -78,12 +80,18 @@ required-features = ["wallet"]
|
|||||||
name = "proof-selection"
|
name = "proof-selection"
|
||||||
required-features = ["wallet"]
|
required-features = ["wallet"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "auth_wallet"
|
||||||
|
required-features = ["wallet", "auth"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = "0.8.5"
|
rand.workspace = true
|
||||||
cdk-sqlite.workspace = true
|
cdk-sqlite.workspace = true
|
||||||
bip39.workspace = true
|
bip39.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
criterion = "0.5.1"
|
criterion = "0.5.1"
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "dhke_benchmarks"
|
name = "dhke_benchmarks"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ async fn main() {
|
|||||||
|
|
||||||
let localstore = memory::empty().await.unwrap();
|
let localstore = memory::empty().await.unwrap();
|
||||||
|
|
||||||
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();
|
let quote = wallet.mint_quote(amount).await.unwrap();
|
||||||
|
|
||||||
|
|||||||
150
crates/cdk/examples/auth_wallet.rs
Normal file
150
crates/cdk/examples/auth_wallet.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use cdk::amount::SplitTarget;
|
||||||
|
use cdk::error::Error;
|
||||||
|
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
|
||||||
|
use cdk::wallet::{SendOptions, Wallet, WalletSubscription};
|
||||||
|
use cdk::{Amount, OidcClient};
|
||||||
|
use cdk_common::{MintInfo, ProofsMethods};
|
||||||
|
use cdk_sqlite::wallet::memory;
|
||||||
|
use rand::Rng;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
const TEST_USERNAME: &str = "cdk-test";
|
||||||
|
const TEST_PASSWORD: &str = "cdkpassword";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Error> {
|
||||||
|
// Set up logging
|
||||||
|
let default_filter = "debug";
|
||||||
|
let sqlx_filter = "sqlx=warn,hyper_util=warn,reqwest=warn,rustls=warn";
|
||||||
|
let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
|
||||||
|
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||||
|
|
||||||
|
// Initialize the memory store for the wallet
|
||||||
|
let localstore = memory::empty().await?;
|
||||||
|
|
||||||
|
// Generate a random seed for the wallet
|
||||||
|
let seed = rand::thread_rng().gen::<[u8; 32]>();
|
||||||
|
|
||||||
|
// Define the mint URL and currency unit
|
||||||
|
let mint_url = "http://127.0.0.1:8085";
|
||||||
|
let unit = CurrencyUnit::Sat;
|
||||||
|
let amount = Amount::from(50);
|
||||||
|
|
||||||
|
// Create a new wallet
|
||||||
|
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?;
|
||||||
|
|
||||||
|
let mint_info = wallet
|
||||||
|
.get_mint_info()
|
||||||
|
.await
|
||||||
|
.expect("mint info")
|
||||||
|
.expect("could not get mint info");
|
||||||
|
|
||||||
|
// Request a mint quote from the wallet
|
||||||
|
let quote = wallet.mint_quote(amount, None).await;
|
||||||
|
|
||||||
|
println!("Minting nuts ... {:?}", quote);
|
||||||
|
|
||||||
|
// Getting the CAT token is not inscope of cdk and expected to be handled by the implemntor
|
||||||
|
// We just use this helper fn with password auth for testing
|
||||||
|
let access_token = get_access_token(&mint_info).await;
|
||||||
|
|
||||||
|
wallet.set_cat(access_token).await.unwrap();
|
||||||
|
|
||||||
|
wallet
|
||||||
|
.mint_blind_auth(10.into())
|
||||||
|
.await
|
||||||
|
.expect("Could not mint blind auth");
|
||||||
|
|
||||||
|
// Request a mint quote from the wallet
|
||||||
|
let quote = wallet.mint_quote(amount, None).await?;
|
||||||
|
|
||||||
|
// Subscribe to updates on the mint quote state
|
||||||
|
let mut subscription = wallet
|
||||||
|
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote
|
||||||
|
.id
|
||||||
|
.clone()]))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Wait for the mint quote to be paid
|
||||||
|
while let Some(msg) = subscription.recv().await {
|
||||||
|
if let NotificationPayload::MintQuoteBolt11Response(response) = msg {
|
||||||
|
if response.state == MintQuoteState::Paid {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mint the received amount
|
||||||
|
let receive_amount = wallet.mint("e.id, SplitTarget::default(), None).await?;
|
||||||
|
|
||||||
|
println!("Received: {}", receive_amount.total_amount()?);
|
||||||
|
|
||||||
|
// Get the total balance of the wallet
|
||||||
|
let balance = wallet.total_balance().await?;
|
||||||
|
println!("Wallet balance: {}", balance);
|
||||||
|
|
||||||
|
let prepared_send = wallet
|
||||||
|
.prepare_send(10.into(), SendOptions::default())
|
||||||
|
.await?;
|
||||||
|
let token = wallet.send(prepared_send, None).await?;
|
||||||
|
|
||||||
|
println!("Created token: {}", token);
|
||||||
|
|
||||||
|
let remaining_blind_auth = wallet.get_unspent_auth_proofs().await?.len();
|
||||||
|
|
||||||
|
// We started with 10 blind tokens we expect 8 ath this point
|
||||||
|
// 1 is used for the mint quote + 1 used for the mint
|
||||||
|
// The swap is not expected to use one as it will be offline or we have "/swap" as an unprotected endpoint in the mint config
|
||||||
|
assert_eq!(remaining_blind_auth, 8);
|
||||||
|
|
||||||
|
println!("Remaining blind auth: {}", remaining_blind_auth);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_access_token(mint_info: &MintInfo) -> String {
|
||||||
|
let openid_discovery = mint_info
|
||||||
|
.nuts
|
||||||
|
.nut21
|
||||||
|
.clone()
|
||||||
|
.expect("Nut21 defined")
|
||||||
|
.openid_discovery;
|
||||||
|
|
||||||
|
let oidc_client = OidcClient::new(openid_discovery);
|
||||||
|
|
||||||
|
// Get the token endpoint from the OIDC configuration
|
||||||
|
let token_url = oidc_client
|
||||||
|
.get_oidc_config()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get OIDC config")
|
||||||
|
.token_endpoint;
|
||||||
|
|
||||||
|
// Create the request parameters
|
||||||
|
let params = [
|
||||||
|
("grant_type", "password"),
|
||||||
|
("client_id", "cashu-client"),
|
||||||
|
("username", TEST_USERNAME),
|
||||||
|
("password", TEST_PASSWORD),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make the token request directly
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.post(token_url)
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to send token request");
|
||||||
|
|
||||||
|
let token_response: serde_json::Value = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("Failed to parse token response");
|
||||||
|
|
||||||
|
token_response["access_token"]
|
||||||
|
.as_str()
|
||||||
|
.expect("No access token in response")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ async fn main() -> Result<(), Error> {
|
|||||||
let amount = Amount::from(100);
|
let amount = Amount::from(100);
|
||||||
|
|
||||||
// Create a new wallet
|
// Create a new wallet
|
||||||
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, Some(1))?;
|
let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap();
|
||||||
|
|
||||||
// Request a mint quote from the wallet
|
// Request a mint quote from the wallet
|
||||||
let quote = wallet.mint_quote(amount, None).await?;
|
let quote = wallet.mint_quote(amount, None).await?;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let localstore = memory::empty().await?;
|
let localstore = memory::empty().await?;
|
||||||
|
|
||||||
// Create a new wallet
|
// 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).unwrap();
|
||||||
|
|
||||||
// Amount to mint
|
// Amount to mint
|
||||||
for amount in [64] {
|
for amount in [64] {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let localstore = memory::empty().await?;
|
let localstore = memory::empty().await?;
|
||||||
|
|
||||||
// Create a new wallet
|
// 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).unwrap();
|
||||||
|
|
||||||
// Request a mint quote from the wallet
|
// Request a mint quote from the wallet
|
||||||
let quote = wallet.mint_quote(amount, None).await?;
|
let quote = wallet.mint_quote(amount, None).await?;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
pub mod cdk_database {
|
pub mod cdk_database {
|
||||||
//! CDK Database
|
//! CDK Database
|
||||||
pub use cdk_common::database::Error;
|
pub use cdk_common::database::Error;
|
||||||
|
#[cfg(all(feature = "mint", feature = "auth"))]
|
||||||
|
pub use cdk_common::database::MintAuthDatabase;
|
||||||
#[cfg(feature = "mint")]
|
#[cfg(feature = "mint")]
|
||||||
pub use cdk_common::database::MintDatabase;
|
pub use cdk_common::database::MintDatabase;
|
||||||
#[cfg(feature = "wallet")]
|
#[cfg(feature = "wallet")]
|
||||||
@@ -17,6 +19,12 @@ pub mod mint;
|
|||||||
#[cfg(feature = "wallet")]
|
#[cfg(feature = "wallet")]
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
|
|
||||||
|
#[cfg(all(any(feature = "wallet", feature = "mint"), feature = "auth"))]
|
||||||
|
mod oidc_client;
|
||||||
|
|
||||||
|
#[cfg(all(any(feature = "wallet", feature = "mint"), feature = "auth"))]
|
||||||
|
pub use oidc_client::OidcClient;
|
||||||
|
|
||||||
pub mod pub_sub;
|
pub mod pub_sub;
|
||||||
|
|
||||||
/// Re-export amount type
|
/// Re-export amount type
|
||||||
@@ -45,7 +53,7 @@ pub use wallet::{Wallet, WalletSubscription};
|
|||||||
pub use self::util::SECP256K1;
|
pub use self::util::SECP256K1;
|
||||||
#[cfg(feature = "wallet")]
|
#[cfg(feature = "wallet")]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use self::wallet::client::HttpClient;
|
pub use self::wallet::HttpClient;
|
||||||
|
|
||||||
/// Result
|
/// Result
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|||||||
413
crates/cdk/src/mint/auth/mod.rs
Normal file
413
crates/cdk/src/mint/auth/mod.rs
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
use cdk_common::{CurrencyUnit, MintKeySet};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use super::nut21::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
|
||||||
|
#[instrument(skip(self), fields(endpoint = ?method))]
|
||||||
|
pub async fn is_protected(
|
||||||
|
&self,
|
||||||
|
method: &ProtectedEndpoint,
|
||||||
|
) -> Result<Option<AuthRequired>, Error> {
|
||||||
|
if let Some(auth_db) = self.auth_localstore.as_ref() {
|
||||||
|
Ok(auth_db.get_auth_for_endpoint(*method).await?)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify Clear auth
|
||||||
|
#[instrument(skip_all, fields(token_len = token.len()))]
|
||||||
|
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<MintKeySet, Error> {
|
||||||
|
{
|
||||||
|
if let Some(keyset) = self.keysets.read().await.get(id) {
|
||||||
|
return Ok(keyset.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Keyset {:?} not found in memory, attempting to load from storage",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut keysets = self.keysets.write().await;
|
||||||
|
|
||||||
|
// Get auth_localstore reference
|
||||||
|
let auth_localstore = match self.auth_localstore.as_ref() {
|
||||||
|
Some(store) => store,
|
||||||
|
None => {
|
||||||
|
tracing::error!("Auth localstore is not configured");
|
||||||
|
return Err(Error::AmountKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get keyset info from storage
|
||||||
|
let keyset_info = match auth_localstore.get_keyset_info(id).await {
|
||||||
|
Ok(Some(info)) => {
|
||||||
|
tracing::debug!("Found keyset info in storage for ID {:?}", id);
|
||||||
|
info
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::error!("Keyset with ID {:?} not found in storage", id);
|
||||||
|
return Err(Error::KeysetUnknown(*id));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error retrieving keyset info from storage: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = keyset_info.id;
|
||||||
|
tracing::info!("Generating and inserting keyset {:?} into memory", id);
|
||||||
|
let keyset = self.generate_keyset(keyset_info);
|
||||||
|
|
||||||
|
keysets.insert(id, keyset.clone());
|
||||||
|
tracing::debug!("Keyset {:?} successfully loaded", id);
|
||||||
|
Ok(keyset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify Blind auth
|
||||||
|
#[instrument(skip(self, token))]
|
||||||
|
pub async fn verify_blind_auth(&self, token: &BlindAuthToken) -> Result<(), Error> {
|
||||||
|
let proof = &token.auth_proof;
|
||||||
|
let keyset_id = proof.keyset_id;
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
"Starting blind auth verification for keyset ID: {:?}",
|
||||||
|
keyset_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure the keyset is loaded
|
||||||
|
let keyset = self
|
||||||
|
.ensure_blind_auth_keyset_loaded(&keyset_id)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Failed to load keyset: {:?}", err);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Verify keyset is for auth
|
||||||
|
if keyset.unit != CurrencyUnit::Auth {
|
||||||
|
tracing::warn!(
|
||||||
|
"Blind auth attempted with non-auth keyset. Found unit: {:?}",
|
||||||
|
keyset.unit
|
||||||
|
);
|
||||||
|
return Err(Error::BlindAuthFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the keypair for amount 1
|
||||||
|
let keypair = match keyset.keys.get(&Amount::from(1)) {
|
||||||
|
Some(key_pair) => key_pair,
|
||||||
|
None => {
|
||||||
|
tracing::error!("No keypair found for amount 1 in keyset {:?}", keyset_id);
|
||||||
|
return Err(Error::AmountKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify the message
|
||||||
|
match verify_message(&keypair.secret_key, proof.c, proof.secret.as_bytes()) {
|
||||||
|
Ok(_) => {
|
||||||
|
tracing::trace!(
|
||||||
|
"Blind signature verification successful for keyset ID: {:?}",
|
||||||
|
keyset_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Blind signature verification failed: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify Auth
|
||||||
|
///
|
||||||
|
/// If it is a blind auth this will also burn the proof
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn verify_auth(
|
||||||
|
&self,
|
||||||
|
auth_token: Option<AuthToken>,
|
||||||
|
endpoint: &ProtectedEndpoint,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if let Some(auth_required) = self.is_protected(endpoint).await? {
|
||||||
|
tracing::info!(
|
||||||
|
"Auth required for endpoint: {:?}, type: {:?}",
|
||||||
|
endpoint,
|
||||||
|
auth_required
|
||||||
|
);
|
||||||
|
|
||||||
|
let auth_token = match auth_token {
|
||||||
|
Some(token) => token,
|
||||||
|
None => match auth_required {
|
||||||
|
AuthRequired::Clear => {
|
||||||
|
tracing::warn!(
|
||||||
|
"No auth token provided for protected endpoint: {:?}, expected clear auth.",
|
||||||
|
endpoint
|
||||||
|
);
|
||||||
|
return Err(Error::ClearAuthRequired);
|
||||||
|
}
|
||||||
|
AuthRequired::Blind => {
|
||||||
|
tracing::warn!(
|
||||||
|
"No auth token provided for protected endpoint: {:?}, expected blind auth.",
|
||||||
|
endpoint
|
||||||
|
);
|
||||||
|
return Err(Error::BlindAuthRequired);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match (auth_required, auth_token) {
|
||||||
|
(AuthRequired::Clear, AuthToken::ClearAuth(token)) => {
|
||||||
|
tracing::debug!("Verifying clear auth token");
|
||||||
|
match self.verify_clear_auth(token.clone()).await {
|
||||||
|
Ok(_) => tracing::info!("Clear auth verification successful"),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Clear auth verification failed: {:?}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(AuthRequired::Blind, AuthToken::BlindAuth(token)) => {
|
||||||
|
tracing::debug!(
|
||||||
|
"Verifying blind auth token with keyset_id: {:?}",
|
||||||
|
token.auth_proof.keyset_id
|
||||||
|
);
|
||||||
|
|
||||||
|
match self.verify_blind_auth(&token).await {
|
||||||
|
Ok(_) => tracing::debug!("Blind auth signature verification successful"),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Blind auth verification failed: {:?}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_proof = token.auth_proof;
|
||||||
|
|
||||||
|
self.check_blind_auth_proof_spendable(auth_proof)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Failed to spend blind auth proof: {:?}", err);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
(AuthRequired::Blind, other) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Blind auth required but received different auth type: {:?}",
|
||||||
|
other
|
||||||
|
);
|
||||||
|
return Err(Error::BlindAuthRequired);
|
||||||
|
}
|
||||||
|
(AuthRequired::Clear, other) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Clear auth required but received different auth type: {:?}",
|
||||||
|
other
|
||||||
|
);
|
||||||
|
return Err(Error::ClearAuthRequired);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::debug!("No auth required for endpoint: {:?}", endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Auth verification completed successfully");
|
||||||
|
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> {
|
||||||
|
tracing::trace!(
|
||||||
|
"Checking if blind auth proof is spendable for keyset ID: {:?}",
|
||||||
|
proof.keyset_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get auth_localstore reference
|
||||||
|
let auth_localstore = match self.auth_localstore.as_ref() {
|
||||||
|
Some(store) => store,
|
||||||
|
None => {
|
||||||
|
tracing::error!("Auth localstore is not configured");
|
||||||
|
return Err(Error::AmountKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate the Y value for the proof
|
||||||
|
let y = proof.y().map_err(|err| {
|
||||||
|
tracing::error!("Failed to calculate Y value for proof: {:?}", err);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Add proof to the database
|
||||||
|
auth_localstore
|
||||||
|
.add_proof(proof.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Failed to add proof to database: {:?}", err);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Update proof state to spent
|
||||||
|
let state = match auth_localstore.update_proof_state(&y, State::Spent).await {
|
||||||
|
Ok(state) => {
|
||||||
|
tracing::debug!(
|
||||||
|
"Successfully updated proof state to SPENT, previous state: {:?}",
|
||||||
|
state
|
||||||
|
);
|
||||||
|
state
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to update proof state: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check previous state
|
||||||
|
match state {
|
||||||
|
Some(State::Spent) => {
|
||||||
|
tracing::warn!("Token already spent: {:?}", y);
|
||||||
|
return Err(Error::TokenAlreadySpent);
|
||||||
|
}
|
||||||
|
Some(State::Pending) => {
|
||||||
|
tracing::warn!("Token is pending: {:?}", y);
|
||||||
|
return Err(Error::TokenPending);
|
||||||
|
}
|
||||||
|
Some(other_state) => {
|
||||||
|
tracing::trace!("Token was in state {:?}, now marked as spent", other_state);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::trace!("Token was in state None, now marked as spent");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Blind Sign
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn auth_blind_sign(
|
||||||
|
&self,
|
||||||
|
blinded_message: &BlindedMessage,
|
||||||
|
) -> Result<BlindSignature, Error> {
|
||||||
|
let BlindedMessage {
|
||||||
|
amount,
|
||||||
|
blinded_secret,
|
||||||
|
keyset_id,
|
||||||
|
..
|
||||||
|
} = blinded_message;
|
||||||
|
|
||||||
|
// Ensure the keyset is loaded
|
||||||
|
let keyset = match self.ensure_blind_auth_keyset_loaded(keyset_id).await {
|
||||||
|
Ok(keyset) => keyset,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to load keyset: {:?}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get auth_localstore reference
|
||||||
|
let auth_localstore = match self.auth_localstore.as_ref() {
|
||||||
|
Some(store) => store,
|
||||||
|
None => {
|
||||||
|
tracing::error!("Auth localstore is not configured");
|
||||||
|
return Err(Error::AuthSettingsUndefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get keyset info
|
||||||
|
let keyset_info = match auth_localstore.get_keyset_info(keyset_id).await {
|
||||||
|
Ok(Some(info)) => info,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::error!("Keyset with ID {:?} not found in storage", keyset_id);
|
||||||
|
return Err(Error::UnknownKeySet);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error retrieving keyset info from storage: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get active keyset ID
|
||||||
|
let active = match auth_localstore.get_active_keyset_id().await {
|
||||||
|
Ok(Some(id)) => id,
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::error!("No active keyset found");
|
||||||
|
return Err(Error::InactiveKeyset);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error retrieving active keyset ID: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check that the keyset is active and should be used to sign
|
||||||
|
if keyset_info.id.ne(&active) {
|
||||||
|
tracing::warn!(
|
||||||
|
"Keyset {:?} is not active. Active keyset is {:?}",
|
||||||
|
keyset_info.id,
|
||||||
|
active
|
||||||
|
);
|
||||||
|
return Err(Error::InactiveKeyset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the keypair for the specified amount
|
||||||
|
let key_pair = match keyset.keys.get(amount) {
|
||||||
|
Some(key_pair) => key_pair,
|
||||||
|
None => {
|
||||||
|
tracing::error!(
|
||||||
|
"No keypair found for amount {:?} in keyset {:?}",
|
||||||
|
amount,
|
||||||
|
keyset_id
|
||||||
|
);
|
||||||
|
return Err(Error::AmountKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign the message
|
||||||
|
let c = match sign_message(&key_pair.secret_key, blinded_secret) {
|
||||||
|
Ok(signature) => signature,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to sign message: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create blinded signature
|
||||||
|
let blinded_signature = match BlindSignature::new(
|
||||||
|
*amount,
|
||||||
|
c,
|
||||||
|
keyset_info.id,
|
||||||
|
&blinded_message.blinded_secret,
|
||||||
|
key_pair.secret_key.clone(),
|
||||||
|
) {
|
||||||
|
Ok(sig) => sig,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to create blinded signature: {:?}", e);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
"Blind signing completed successfully for keyset ID: {:?}",
|
||||||
|
keyset_id
|
||||||
|
);
|
||||||
|
Ok(blinded_signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,16 @@ use bitcoin::bip32::DerivationPath;
|
|||||||
use cdk_common::database::{self, MintDatabase};
|
use cdk_common::database::{self, MintDatabase};
|
||||||
use cdk_common::error::Error;
|
use cdk_common::error::Error;
|
||||||
use cdk_common::payment::Bolt11Settings;
|
use cdk_common::payment::Bolt11Settings;
|
||||||
|
use cdk_common::{nut21, nut22};
|
||||||
|
|
||||||
use super::nut17::SupportedMethods;
|
use super::nut17::SupportedMethods;
|
||||||
use super::nut19::{self, CachedEndpoint};
|
use super::nut19::{self, CachedEndpoint};
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use super::MintAuthDatabase;
|
||||||
use super::Nuts;
|
use super::Nuts;
|
||||||
use crate::amount::Amount;
|
use crate::amount::Amount;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::cdk_database;
|
||||||
use crate::cdk_payment::{self, MintPayment};
|
use crate::cdk_payment::{self, MintPayment};
|
||||||
use crate::mint::Mint;
|
use crate::mint::Mint;
|
||||||
use crate::nuts::{
|
use crate::nuts::{
|
||||||
@@ -28,6 +33,9 @@ pub struct MintBuilder {
|
|||||||
pub mint_info: MintInfo,
|
pub mint_info: MintInfo,
|
||||||
/// Mint Storage backend
|
/// Mint Storage backend
|
||||||
localstore: Option<Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>>,
|
localstore: Option<Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>>,
|
||||||
|
/// Mint Storage backend
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
auth_localstore: Option<Arc<dyn MintAuthDatabase<Err = cdk_database::Error> + Send + Sync>>,
|
||||||
/// Ln backends for mint
|
/// Ln backends for mint
|
||||||
ln: Option<
|
ln: Option<
|
||||||
HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
|
HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
|
||||||
@@ -35,6 +43,8 @@ pub struct MintBuilder {
|
|||||||
seed: Option<Vec<u8>>,
|
seed: Option<Vec<u8>>,
|
||||||
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
||||||
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
|
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
|
||||||
|
// protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
|
||||||
|
openid_discovery: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MintBuilder {
|
impl MintBuilder {
|
||||||
@@ -66,6 +76,22 @@ impl MintBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set auth localstore
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub fn with_auth_localstore(
|
||||||
|
mut self,
|
||||||
|
localstore: Arc<dyn MintAuthDatabase<Err = cdk_database::Error> + Send + Sync>,
|
||||||
|
) -> MintBuilder {
|
||||||
|
self.auth_localstore = Some(localstore);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set Openid discovery url
|
||||||
|
pub fn with_openid_discovery(mut self, openid_discovery: String) -> Self {
|
||||||
|
self.openid_discovery = Some(openid_discovery);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set seed
|
/// Set seed
|
||||||
pub fn with_seed(mut self, seed: Vec<u8>) -> Self {
|
pub fn with_seed(mut self, seed: Vec<u8>) -> Self {
|
||||||
self.seed = Some(seed);
|
self.seed = Some(seed);
|
||||||
@@ -141,6 +167,9 @@ impl MintBuilder {
|
|||||||
method: method.clone(),
|
method: method.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tracing::debug!("Adding ln backed for {}, {}", unit, method);
|
||||||
|
tracing::debug!("with limits {:?}", limits);
|
||||||
|
|
||||||
let mut ln = self.ln.unwrap_or_default();
|
let mut ln = self.ln.unwrap_or_default();
|
||||||
|
|
||||||
let settings = ln_backend.get_settings().await?;
|
let settings = ln_backend.get_settings().await?;
|
||||||
@@ -235,17 +264,73 @@ impl MintBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set clear auth settings
|
||||||
|
pub fn set_clear_auth_settings(mut self, openid_discovery: String, client_id: String) -> Self {
|
||||||
|
let mut nuts = self.mint_info.nuts;
|
||||||
|
|
||||||
|
nuts.nut21 = Some(nut21::Settings::new(
|
||||||
|
openid_discovery.clone(),
|
||||||
|
client_id,
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
|
||||||
|
self.openid_discovery = Some(openid_discovery);
|
||||||
|
|
||||||
|
self.mint_info.nuts = nuts;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set blind auth settings
|
||||||
|
pub fn set_blind_auth_settings(mut self, bat_max_mint: u64) -> Self {
|
||||||
|
let mut nuts = self.mint_info.nuts;
|
||||||
|
|
||||||
|
nuts.nut22 = Some(nut22::Settings::new(bat_max_mint, vec![]));
|
||||||
|
|
||||||
|
self.mint_info.nuts = nuts;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Build mint
|
/// Build mint
|
||||||
pub async fn build(&self) -> anyhow::Result<Mint> {
|
pub async fn build(&self) -> anyhow::Result<Mint> {
|
||||||
let localstore = self
|
let localstore = self
|
||||||
.localstore
|
.localstore
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or(anyhow!("Localstore not set"))?;
|
.ok_or(anyhow!("Localstore not set"))?;
|
||||||
|
let seed = self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?;
|
||||||
|
let ln = self.ln.clone().ok_or(anyhow!("Ln backends not set"))?;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
if let Some(openid_discovery) = &self.openid_discovery {
|
||||||
|
let auth_localstore = self
|
||||||
|
.auth_localstore
|
||||||
|
.clone()
|
||||||
|
.ok_or(anyhow!("Auth localstore not set"))?;
|
||||||
|
|
||||||
|
return Ok(Mint::new_with_auth(
|
||||||
|
seed,
|
||||||
|
localstore,
|
||||||
|
auth_localstore,
|
||||||
|
ln,
|
||||||
|
self.supported_units.clone(),
|
||||||
|
self.custom_paths.clone(),
|
||||||
|
openid_discovery.clone(),
|
||||||
|
)
|
||||||
|
.await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
if self.openid_discovery.is_some() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"OpenID discovery URL provided but auth feature is not enabled"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Mint::new(
|
Ok(Mint::new(
|
||||||
self.seed.as_ref().ok_or(anyhow!("Mint seed not set"))?,
|
seed,
|
||||||
localstore,
|
localstore,
|
||||||
self.ln.clone().ok_or(anyhow!("Ln backends not set"))?,
|
ln,
|
||||||
self.supported_units.clone(),
|
self.supported_units.clone(),
|
||||||
self.custom_paths.clone(),
|
self.custom_paths.clone(),
|
||||||
)
|
)
|
||||||
|
|||||||
54
crates/cdk/src/mint/issue/auth.rs
Normal file
54
crates/cdk/src/mint/issue/auth.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::mint::nut22::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<MintBolt11Response, Error> {
|
||||||
|
let cat = if let AuthToken::ClearAuth(cat) = auth_token {
|
||||||
|
cat
|
||||||
|
} else {
|
||||||
|
tracing::debug!("Received blind auth mint without cat");
|
||||||
|
return Err(Error::ClearAuthRequired);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.verify_clear_auth(cat).await?;
|
||||||
|
|
||||||
|
let auth_settings = self
|
||||||
|
.mint_info()
|
||||||
|
.await?
|
||||||
|
.nuts
|
||||||
|
.nut22
|
||||||
|
.ok_or(Error::AuthSettingsUndefined)?;
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,14 @@ use cdk_common::payment::Bolt11Settings;
|
|||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::verification::Verification;
|
use crate::mint::{
|
||||||
use super::{
|
CurrencyUnit, MintBolt11Request, MintBolt11Response, MintQuote, MintQuoteBolt11Request,
|
||||||
nut04, CurrencyUnit, Mint, MintQuote, MintQuoteBolt11Request, MintQuoteBolt11Response,
|
MintQuoteBolt11Response, MintQuoteState, NotificationPayload, PublicKey, Verification,
|
||||||
NotificationPayload, PaymentMethod, PublicKey,
|
|
||||||
};
|
};
|
||||||
use crate::nuts::MintQuoteState;
|
use crate::nuts::PaymentMethod;
|
||||||
use crate::types::PaymentProcessorKey;
|
use crate::types::PaymentProcessorKey;
|
||||||
use crate::util::unix_time;
|
use crate::util::unix_time;
|
||||||
use crate::{ensure_cdk, Amount, Error};
|
use crate::{ensure_cdk, Amount, Error, Mint};
|
||||||
|
|
||||||
impl Mint {
|
impl Mint {
|
||||||
/// Checks that minting is enabled, request is supported unit and within range
|
/// Checks that minting is enabled, request is supported unit and within range
|
||||||
@@ -260,8 +259,8 @@ impl Mint {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn process_mint_request(
|
pub async fn process_mint_request(
|
||||||
&self,
|
&self,
|
||||||
mint_request: nut04::MintBolt11Request<Uuid>,
|
mint_request: MintBolt11Request<Uuid>,
|
||||||
) -> Result<nut04::MintBolt11Response, Error> {
|
) -> Result<MintBolt11Response, Error> {
|
||||||
let mint_quote = self
|
let mint_quote = self
|
||||||
.localstore
|
.localstore
|
||||||
.get_mint_quote(&mint_request.quote)
|
.get_mint_quote(&mint_request.quote)
|
||||||
@@ -356,7 +355,7 @@ impl Mint {
|
|||||||
self.pubsub_manager
|
self.pubsub_manager
|
||||||
.mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
|
.mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
|
||||||
|
|
||||||
Ok(nut04::MintBolt11Response {
|
Ok(MintBolt11Response {
|
||||||
signatures: blind_signatures,
|
signatures: blind_signatures,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
3
crates/cdk/src/mint/issue/mod.rs
Normal file
3
crates/cdk/src/mint/issue/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
|
mod issue_nut04;
|
||||||
65
crates/cdk/src/mint/keysets/auth.rs
Normal file
65
crates/cdk/src/mint/keysets/auth.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
//! Auth keyset functions
|
||||||
|
|
||||||
|
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<KeysResponse, Error> {
|
||||||
|
let active_keyset_id = self
|
||||||
|
.auth_localstore
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::AuthLocalstoreUndefined)?
|
||||||
|
.get_active_keyset_id()
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::AmountKey)?;
|
||||||
|
|
||||||
|
self.ensure_blind_auth_keyset_loaded(&active_keyset_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let keysets = self.keysets.read().await;
|
||||||
|
|
||||||
|
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<KeysetResponse, Error> {
|
||||||
|
let keysets = self
|
||||||
|
.auth_localstore
|
||||||
|
.clone()
|
||||||
|
.ok_or(Error::AuthLocalstoreUndefined)?
|
||||||
|
.get_keyset_infos()
|
||||||
|
.await?;
|
||||||
|
let active_keysets: Id = self
|
||||||
|
.auth_localstore
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::AuthLocalstoreUndefined)?
|
||||||
|
.get_active_keyset_id()
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::NoActiveKeyset)?;
|
||||||
|
|
||||||
|
let keysets = keysets
|
||||||
|
.into_iter()
|
||||||
|
.filter(|k| k.unit == CurrencyUnit::Auth)
|
||||||
|
.map(|k| KeySetInfo {
|
||||||
|
id: k.id,
|
||||||
|
unit: k.unit,
|
||||||
|
active: active_keysets == k.id,
|
||||||
|
input_fee_ppk: k.input_fee_ppk,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(KeysetResponse { keysets })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
|
|
||||||
impl Mint {
|
impl Mint {
|
||||||
/// Initialize keysets and returns a [`Result`] with a tuple of the following:
|
/// Initialize keysets and returns a [`Result`] with a tuple of the following:
|
||||||
/// * a [`HashMap`] mapping each active keyset `Id` to `MintKeySet`
|
/// * a [`HashMap`] mapping each active keyset `Id` to `MintKeySet`
|
||||||
@@ -138,7 +141,10 @@ impl Mint {
|
|||||||
/// clients
|
/// clients
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn pubkeys(&self) -> Result<KeysResponse, Error> {
|
pub async fn pubkeys(&self) -> Result<KeysResponse, Error> {
|
||||||
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();
|
let active_keysets: HashSet<&Id> = active_keysets.values().collect();
|
||||||
|
|
||||||
@@ -174,6 +180,7 @@ impl Mint {
|
|||||||
|
|
||||||
let keysets = keysets
|
let keysets = keysets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|k| k.unit != CurrencyUnit::Auth)
|
||||||
.map(|k| KeySetInfo {
|
.map(|k| KeySetInfo {
|
||||||
id: k.id,
|
id: k.id,
|
||||||
unit: k.unit,
|
unit: k.unit,
|
||||||
@@ -6,9 +6,13 @@ use std::sync::Arc;
|
|||||||
use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
|
use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
|
||||||
use bitcoin::secp256k1::{self, Secp256k1};
|
use bitcoin::secp256k1::{self, Secp256k1};
|
||||||
use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
|
use cdk_common::common::{PaymentProcessorKey, QuoteTTL};
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use cdk_common::database::MintAuthDatabase;
|
||||||
use cdk_common::database::{self, MintDatabase};
|
use cdk_common::database::{self, MintDatabase};
|
||||||
use cdk_common::mint::MintKeySetInfo;
|
use cdk_common::mint::MintKeySetInfo;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use nut21::ProtectedEndpoint;
|
||||||
use subscription::PubSubManager;
|
use subscription::PubSubManager;
|
||||||
use tokio::sync::{Notify, RwLock};
|
use tokio::sync::{Notify, RwLock};
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
@@ -21,14 +25,18 @@ use crate::error::Error;
|
|||||||
use crate::fees::calculate_fee;
|
use crate::fees::calculate_fee;
|
||||||
use crate::nuts::*;
|
use crate::nuts::*;
|
||||||
use crate::util::unix_time;
|
use crate::util::unix_time;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::OidcClient;
|
||||||
use crate::{ensure_cdk, Amount};
|
use crate::{ensure_cdk, Amount};
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub(crate) mod auth;
|
||||||
mod builder;
|
mod builder;
|
||||||
mod check_spendable;
|
mod check_spendable;
|
||||||
|
mod issue;
|
||||||
mod keysets;
|
mod keysets;
|
||||||
mod ln;
|
mod ln;
|
||||||
mod melt;
|
mod melt;
|
||||||
mod mint_nut04;
|
|
||||||
mod start_up_check;
|
mod start_up_check;
|
||||||
pub mod subscription;
|
pub mod subscription;
|
||||||
mod swap;
|
mod swap;
|
||||||
@@ -36,17 +44,23 @@ mod verification;
|
|||||||
|
|
||||||
pub use builder::{MintBuilder, MintMeltLimits};
|
pub use builder::{MintBuilder, MintMeltLimits};
|
||||||
pub use cdk_common::mint::{MeltQuote, MintQuote};
|
pub use cdk_common::mint::{MeltQuote, MintQuote};
|
||||||
|
pub use verification::Verification;
|
||||||
|
|
||||||
/// Cashu Mint
|
/// Cashu Mint
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Mint {
|
pub struct Mint {
|
||||||
/// Mint Storage backend
|
/// Mint Storage backend
|
||||||
pub localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
|
pub localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
|
||||||
|
/// Auth Storage backend (only available with auth feature)
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub auth_localstore: Option<Arc<dyn MintAuthDatabase<Err = database::Error> + Send + Sync>>,
|
||||||
/// Ln backends for mint
|
/// Ln backends for mint
|
||||||
pub ln:
|
pub ln:
|
||||||
HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
|
HashMap<PaymentProcessorKey, Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>>,
|
||||||
/// Subscription manager
|
/// Subscription manager
|
||||||
pub pubsub_manager: Arc<PubSubManager>,
|
pub pubsub_manager: Arc<PubSubManager>,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
oidc_client: Option<OidcClient>,
|
||||||
secp_ctx: Secp256k1<secp256k1::All>,
|
secp_ctx: Secp256k1<secp256k1::All>,
|
||||||
xpriv: Xpriv,
|
xpriv: Xpriv,
|
||||||
keysets: Arc<RwLock<HashMap<Id, MintKeySet>>>,
|
keysets: Arc<RwLock<HashMap<Id, MintKeySet>>>,
|
||||||
@@ -54,8 +68,7 @@ pub struct Mint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Mint {
|
impl Mint {
|
||||||
/// Create new [`Mint`]
|
/// Create new [`Mint`] without authentication
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
seed: &[u8],
|
seed: &[u8],
|
||||||
localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
|
localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
|
||||||
@@ -63,9 +76,63 @@ impl Mint {
|
|||||||
PaymentProcessorKey,
|
PaymentProcessorKey,
|
||||||
Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
|
Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
|
||||||
>,
|
>,
|
||||||
// Hashmap where the key is the unit and value is (input fee ppk, max_order)
|
|
||||||
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
||||||
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
|
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
Self::new_internal(
|
||||||
|
seed,
|
||||||
|
localstore,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
None,
|
||||||
|
ln,
|
||||||
|
supported_units,
|
||||||
|
custom_paths,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new [`Mint`] with authentication support
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub async fn new_with_auth(
|
||||||
|
seed: &[u8],
|
||||||
|
localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
|
||||||
|
auth_localstore: Arc<dyn MintAuthDatabase<Err = database::Error> + Send + Sync>,
|
||||||
|
ln: HashMap<
|
||||||
|
PaymentProcessorKey,
|
||||||
|
Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
|
||||||
|
>,
|
||||||
|
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
||||||
|
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
|
||||||
|
open_id_discovery: String,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
Self::new_internal(
|
||||||
|
seed,
|
||||||
|
localstore,
|
||||||
|
Some(auth_localstore),
|
||||||
|
ln,
|
||||||
|
supported_units,
|
||||||
|
custom_paths,
|
||||||
|
Some(open_id_discovery),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal function to create a new [`Mint`] with shared logic
|
||||||
|
async fn new_internal(
|
||||||
|
seed: &[u8],
|
||||||
|
localstore: Arc<dyn MintDatabase<Err = database::Error> + Send + Sync>,
|
||||||
|
#[cfg(feature = "auth")] auth_localstore: Option<
|
||||||
|
Arc<dyn database::MintAuthDatabase<Err = database::Error> + Send + Sync>,
|
||||||
|
>,
|
||||||
|
ln: HashMap<
|
||||||
|
PaymentProcessorKey,
|
||||||
|
Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
|
||||||
|
>,
|
||||||
|
supported_units: HashMap<CurrencyUnit, (u64, u8)>,
|
||||||
|
custom_paths: HashMap<CurrencyUnit, DerivationPath>,
|
||||||
|
#[cfg(feature = "auth")] open_id_discovery: Option<String>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let secp_ctx = Secp256k1::new();
|
let secp_ctx = Secp256k1::new();
|
||||||
let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
|
let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
|
||||||
@@ -106,6 +173,49 @@ impl Mint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let oidc_client = if let Some(openid_discovery) = open_id_discovery {
|
||||||
|
{
|
||||||
|
tracing::info!("Auth enabled creating auth keysets");
|
||||||
|
let auth_localstore = auth_localstore
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::AuthSettingsUndefined)?;
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(openid_discovery.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
{
|
||||||
|
tracing::error!("CDK must be compiled with auth feature to be used with auth.");
|
||||||
|
return Err(Error::Custom(
|
||||||
|
"Openid passed but cdk compiled without auth.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let keysets = Arc::new(RwLock::new(active_keysets));
|
let keysets = Arc::new(RwLock::new(active_keysets));
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -113,16 +223,56 @@ impl Mint {
|
|||||||
secp_ctx,
|
secp_ctx,
|
||||||
xpriv,
|
xpriv,
|
||||||
localstore,
|
localstore,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
oidc_client,
|
||||||
ln,
|
ln,
|
||||||
keysets,
|
|
||||||
custom_paths,
|
custom_paths,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
auth_localstore,
|
||||||
|
keysets,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get mint info
|
/// Get mint info
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn mint_info(&self) -> Result<MintInfo, Error> {
|
pub async fn mint_info(&self) -> Result<MintInfo, Error> {
|
||||||
Ok(self.localstore.get_mint_info().await?)
|
let mint_info = self.localstore.get_mint_info().await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let mint_info = if let Some(auth_db) = self.auth_localstore.as_ref() {
|
||||||
|
let mut mint_info = mint_info;
|
||||||
|
let auth_endpoints = auth_db.get_auth_for_endpoints().await?;
|
||||||
|
|
||||||
|
let mut clear_auth_endpoints: Vec<ProtectedEndpoint> = vec![];
|
||||||
|
let mut blind_auth_endpoints: Vec<ProtectedEndpoint> = vec![];
|
||||||
|
|
||||||
|
for (endpoint, auth) in auth_endpoints {
|
||||||
|
match auth {
|
||||||
|
Some(AuthRequired::Clear) => {
|
||||||
|
clear_auth_endpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
Some(AuthRequired::Blind) => {
|
||||||
|
blind_auth_endpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mint_info.nuts.nut21 = mint_info.nuts.nut21.map(|mut a| {
|
||||||
|
a.protected_endpoints = clear_auth_endpoints;
|
||||||
|
a
|
||||||
|
});
|
||||||
|
|
||||||
|
mint_info.nuts.nut22 = mint_info.nuts.nut22.map(|mut a| {
|
||||||
|
a.protected_endpoints = blind_auth_endpoints;
|
||||||
|
a
|
||||||
|
});
|
||||||
|
mint_info
|
||||||
|
} else {
|
||||||
|
mint_info
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(mint_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set mint info
|
/// Set mint info
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ use tracing::instrument;
|
|||||||
|
|
||||||
use super::{Error, Mint};
|
use super::{Error, Mint};
|
||||||
|
|
||||||
|
/// Verification result
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
pub struct Verification {
|
pub struct Verification {
|
||||||
|
/// Value in request
|
||||||
pub amount: Amount,
|
pub amount: Amount,
|
||||||
|
/// Unit of request
|
||||||
pub unit: Option<CurrencyUnit>,
|
pub unit: Option<CurrencyUnit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
240
crates/cdk/src/oidc_client.rs
Normal file
240
crates/cdk/src/oidc_client.rs
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
//! 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;
|
||||||
|
#[cfg(feature = "wallet")]
|
||||||
|
use serde::Serialize;
|
||||||
|
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<Error> for cdk_common::error::Error {
|
||||||
|
fn from(value: Error) -> Self {
|
||||||
|
tracing::debug!("Clear auth verification failed: {}", value);
|
||||||
|
cdk_common::error::Error::ClearAuthFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open Id Config
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct OidcConfig {
|
||||||
|
pub jwks_uri: String,
|
||||||
|
pub issuer: String,
|
||||||
|
pub token_endpoint: String,
|
||||||
|
pub device_authorization_endpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Http Client
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OidcClient {
|
||||||
|
client: Client,
|
||||||
|
openid_discovery: String,
|
||||||
|
oidc_config: Arc<RwLock<Option<OidcConfig>>>,
|
||||||
|
jwks_set: Arc<RwLock<Option<JwkSet>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wallet")]
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum GrantType {
|
||||||
|
RefreshToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wallet")]
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct AccessTokenRequest {
|
||||||
|
pub grant_type: GrantType,
|
||||||
|
pub client_id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wallet")]
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct RefreshTokenRequest {
|
||||||
|
pub grant_type: GrantType,
|
||||||
|
pub client_id: String,
|
||||||
|
pub refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "wallet")]
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct TokenResponse {
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
pub expires_in: Option<i64>,
|
||||||
|
pub token_type: 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<OidcConfig, Error> {
|
||||||
|
tracing::debug!("Getting oidc config");
|
||||||
|
let oidc_config = self
|
||||||
|
.client
|
||||||
|
.get(&self.openid_discovery)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<OidcConfig>()
|
||||||
|
.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<JwkSet, Error> {
|
||||||
|
tracing::debug!("Getting jwks set");
|
||||||
|
let jwks_set = self
|
||||||
|
.client
|
||||||
|
.get(jwks_uri)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<JwkSet>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut current_set = self.jwks_set.write().await;
|
||||||
|
|
||||||
|
*current_set = Some(jwks_set.clone());
|
||||||
|
|
||||||
|
Ok(jwks_set)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify cat token
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
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;
|
||||||
|
validation.validate_aud = false;
|
||||||
|
validation.set_issuer(&[oidc_config.issuer]);
|
||||||
|
validation
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) =
|
||||||
|
decode::<HashMap<String, serde_json::Value>>(cat_jwt, &decoding_key, &validation)
|
||||||
|
{
|
||||||
|
tracing::debug!("Could not verify cat: {}", err);
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get new access token using refresh token
|
||||||
|
#[cfg(feature = "wallet")]
|
||||||
|
pub async fn refresh_access_token(
|
||||||
|
&self,
|
||||||
|
client_id: String,
|
||||||
|
refresh_token: String,
|
||||||
|
) -> Result<TokenResponse, Error> {
|
||||||
|
let token_url = self.get_oidc_config().await?.token_endpoint;
|
||||||
|
|
||||||
|
let request = RefreshTokenRequest {
|
||||||
|
grant_type: GrantType::RefreshToken,
|
||||||
|
client_id,
|
||||||
|
refresh_token,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(token_url)
|
||||||
|
.form(&request)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<TokenResponse>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
crates/cdk/src/wallet/auth/auth_connector.rs
Normal file
30
crates/cdk/src/wallet/auth/auth_connector.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use cdk_common::{AuthToken, MintInfo};
|
||||||
|
|
||||||
|
use super::Error;
|
||||||
|
use crate::nuts::{Id, KeySet, KeysetResponse, MintAuthRequest, MintBolt11Response};
|
||||||
|
|
||||||
|
/// 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 AuthMintConnector: Debug {
|
||||||
|
/// Get the current auth token
|
||||||
|
async fn get_auth_token(&self) -> Result<AuthToken, Error>;
|
||||||
|
|
||||||
|
/// Set a new auth token
|
||||||
|
async fn set_auth_token(&self, token: AuthToken) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Get Mint Info [NUT-06]
|
||||||
|
async fn get_mint_info(&self) -> Result<MintInfo, Error>;
|
||||||
|
/// Get Blind Auth Keyset
|
||||||
|
async fn get_mint_blind_auth_keyset(&self, keyset_id: Id) -> Result<KeySet, Error>;
|
||||||
|
/// Get Blind Auth keysets
|
||||||
|
async fn get_mint_blind_auth_keysets(&self) -> Result<KeysetResponse, Error>;
|
||||||
|
/// Post mint blind auth
|
||||||
|
async fn post_mint_blind_auth(
|
||||||
|
&self,
|
||||||
|
request: MintAuthRequest,
|
||||||
|
) -> Result<MintBolt11Response, Error>;
|
||||||
|
}
|
||||||
427
crates/cdk/src/wallet/auth/auth_wallet.rs
Normal file
427
crates/cdk/src/wallet/auth/auth_wallet.rs
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use cdk_common::database::{self, WalletDatabase};
|
||||||
|
use cdk_common::mint_url::MintUrl;
|
||||||
|
use cdk_common::{AuthProof, Id, Keys, MintInfo};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use super::AuthMintConnector;
|
||||||
|
use crate::amount::SplitTarget;
|
||||||
|
use crate::dhke::construct_proofs;
|
||||||
|
use crate::nuts::nut22::MintAuthRequest;
|
||||||
|
use crate::nuts::{
|
||||||
|
nut12, AuthRequired, AuthToken, BlindAuthToken, CurrencyUnit, KeySetInfo, PreMintSecrets,
|
||||||
|
Proofs, ProtectedEndpoint, State,
|
||||||
|
};
|
||||||
|
use crate::types::ProofInfo;
|
||||||
|
use crate::wallet::mint_connector::AuthHttpClient;
|
||||||
|
use crate::{Amount, Error, OidcClient};
|
||||||
|
|
||||||
|
/// JWT Claims structure for decoding tokens
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Claims {
|
||||||
|
/// Subject
|
||||||
|
sub: Option<String>,
|
||||||
|
/// Expiration time (as UTC timestamp)
|
||||||
|
exp: Option<u64>,
|
||||||
|
/// Issued at (as UTC timestamp)
|
||||||
|
iat: Option<u64>,
|
||||||
|
}
|
||||||
|
/// CDK Auth Wallet
|
||||||
|
///
|
||||||
|
/// A [`AuthWallet`] is for auth operations with a single mint.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AuthWallet {
|
||||||
|
/// Mint Url
|
||||||
|
pub mint_url: MintUrl,
|
||||||
|
/// Storage backend
|
||||||
|
pub localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
|
||||||
|
/// Protected methods
|
||||||
|
pub protected_endpoints: Arc<RwLock<HashMap<ProtectedEndpoint, AuthRequired>>>,
|
||||||
|
/// Refresh token for auth
|
||||||
|
refresh_token: Arc<RwLock<Option<String>>>,
|
||||||
|
client: Arc<dyn AuthMintConnector + Send + Sync>,
|
||||||
|
/// OIDC client for authentication
|
||||||
|
oidc_client: Arc<RwLock<Option<OidcClient>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthWallet {
|
||||||
|
/// Create a new [`AuthWallet`] instance
|
||||||
|
pub fn new(
|
||||||
|
mint_url: MintUrl,
|
||||||
|
cat: Option<AuthToken>,
|
||||||
|
localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
|
||||||
|
protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
|
||||||
|
oidc_client: Option<OidcClient>,
|
||||||
|
) -> Self {
|
||||||
|
let http_client = Arc::new(AuthHttpClient::new(mint_url.clone(), cat));
|
||||||
|
Self {
|
||||||
|
mint_url,
|
||||||
|
localstore,
|
||||||
|
protected_endpoints: Arc::new(RwLock::new(protected_endpoints)),
|
||||||
|
refresh_token: Arc::new(RwLock::new(None)),
|
||||||
|
client: http_client,
|
||||||
|
oidc_client: Arc::new(RwLock::new(oidc_client)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current auth token
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_auth_token(&self) -> Result<AuthToken, Error> {
|
||||||
|
self.client.get_auth_token().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a new auth token
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn verify_cat(&self, token: AuthToken) -> Result<(), Error> {
|
||||||
|
match &token {
|
||||||
|
AuthToken::ClearAuth(clear_token) => {
|
||||||
|
if let Some(oidc) = self.oidc_client.read().await.as_ref() {
|
||||||
|
oidc.verify_cat(clear_token).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
AuthToken::BlindAuth(_) => Err(Error::Custom(
|
||||||
|
"Cannot set blind auth token directly".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a new auth token
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn set_auth_token(&self, token: AuthToken) -> Result<(), Error> {
|
||||||
|
match &token {
|
||||||
|
AuthToken::ClearAuth(clear_token) => {
|
||||||
|
if let Some(oidc) = self.oidc_client.read().await.as_ref() {
|
||||||
|
oidc.verify_cat(clear_token).await?;
|
||||||
|
}
|
||||||
|
self.client.set_auth_token(token).await
|
||||||
|
}
|
||||||
|
AuthToken::BlindAuth(_) => Err(Error::Custom(
|
||||||
|
"Cannot set blind auth token directly".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current refresh token if one exists
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_refresh_token(&self) -> Option<String> {
|
||||||
|
self.refresh_token.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a new refresh token
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn set_refresh_token(&self, token: Option<String>) {
|
||||||
|
*self.refresh_token.write().await = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the OIDC client if one exists
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_oidc_client(&self) -> Option<OidcClient> {
|
||||||
|
self.oidc_client.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a new OIDC client
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn set_oidc_client(&self, client: Option<OidcClient>) {
|
||||||
|
*self.oidc_client.write().await = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh the access token using the stored refresh token
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn refresh_access_token(&self) -> Result<(), Error> {
|
||||||
|
if let Some(oidc) = self.oidc_client.read().await.as_ref() {
|
||||||
|
if let Some(refresh_token) = self.get_refresh_token().await {
|
||||||
|
let mint_info = self
|
||||||
|
.get_mint_info()
|
||||||
|
.await?
|
||||||
|
.ok_or(Error::CouldNotGetMintInfo)?;
|
||||||
|
let token_response = oidc
|
||||||
|
.refresh_access_token(
|
||||||
|
mint_info.client_id().ok_or(Error::CouldNotGetMintInfo)?,
|
||||||
|
refresh_token,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Store new refresh token if provided
|
||||||
|
self.set_refresh_token(token_response.refresh_token).await;
|
||||||
|
|
||||||
|
// Set new access token
|
||||||
|
self.set_auth_token(AuthToken::ClearAuth(token_response.access_token))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::Custom(
|
||||||
|
"No refresh token or OIDC client available".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query mint for current mint information
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, Error> {
|
||||||
|
self.client.get_mint_info().await.map(Some).or(Ok(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get keys for mint keyset
|
||||||
|
///
|
||||||
|
/// Selected keys from localstore if they are already known
|
||||||
|
/// If they are not known queries mint for keyset id and stores the [`Keys`]
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
|
||||||
|
let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? {
|
||||||
|
keys
|
||||||
|
} else {
|
||||||
|
let keys = self.client.get_mint_blind_auth_keyset(keyset_id).await?;
|
||||||
|
|
||||||
|
keys.verify_id()?;
|
||||||
|
|
||||||
|
self.localstore.add_keys(keys.keys.clone()).await?;
|
||||||
|
|
||||||
|
keys.keys
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Vec<KeySetInfo>, 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::<Vec<KeySetInfo>>();
|
||||||
|
|
||||||
|
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<KeySetInfo, Error> {
|
||||||
|
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<Vec<AuthProof>, Error> {
|
||||||
|
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.try_into())
|
||||||
|
.collect::<Result<Vec<AuthProof>, _>>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if and what kind of auth is required for a method
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn is_protected(&self, method: &ProtectedEndpoint) -> Option<AuthRequired> {
|
||||||
|
let protected_endpoints = self.protected_endpoints.read().await;
|
||||||
|
|
||||||
|
protected_endpoints.get(method).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Auth Token
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_blind_auth_token(&self) -> Result<Option<AuthToken>, 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(),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auth for request
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_auth_for_request(
|
||||||
|
&self,
|
||||||
|
method: &ProtectedEndpoint,
|
||||||
|
) -> Result<Option<AuthToken>, Error> {
|
||||||
|
match self.is_protected(method).await {
|
||||||
|
Some(auth) => match auth {
|
||||||
|
AuthRequired::Clear => self.client.get_auth_token().await.map(Some),
|
||||||
|
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<Proofs, Error> {
|
||||||
|
tracing::debug!("Minting {} blind auth proofs", amount);
|
||||||
|
// 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 auth_token = self.client.get_auth_token().await?;
|
||||||
|
|
||||||
|
let active_keyset_id = self.get_active_mint_blind_auth_keysets().await?;
|
||||||
|
tracing::debug!("Active ketset: {:?}", active_keyset_id);
|
||||||
|
|
||||||
|
match &auth_token {
|
||||||
|
AuthToken::ClearAuth(cat) => {
|
||||||
|
if cat.is_empty() {
|
||||||
|
tracing::warn!("Auth Cat is not set");
|
||||||
|
return Err(Error::ClearAuthRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = self.verify_cat(auth_token).await {
|
||||||
|
tracing::warn!("Current cat is invalid {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_refresh;
|
||||||
|
|
||||||
|
{
|
||||||
|
has_refresh = self.refresh_token.read().await.is_some();
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_refresh {
|
||||||
|
tracing::info!("Attempting to refresh using refresh token");
|
||||||
|
self.refresh_access_token().await?;
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
"Wallet cat is invalid and there is no refresh token please reauth"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AuthToken::BlindAuth(_) => {
|
||||||
|
tracing::error!("Blind auth set as client cat");
|
||||||
|
return Err(Error::ClearAuthFailed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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).await?;
|
||||||
|
|
||||||
|
let keys = self.get_keyset_keys(active_keyset_id).await?;
|
||||||
|
|
||||||
|
// Verify the signature DLEQ is valid
|
||||||
|
{
|
||||||
|
assert!(mint_res.signatures.len() == premint_secrets.secrets.len());
|
||||||
|
for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
|
||||||
|
let keys = self.get_keyset_keys(sig.keyset_id).await?;
|
||||||
|
let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
|
||||||
|
match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(nut12::Error::MissingDleqProof) => {
|
||||||
|
tracing::warn!("Signature for bat returned without dleq proof.");
|
||||||
|
return Err(Error::DleqProofNotProvided);
|
||||||
|
}
|
||||||
|
Err(_) => return Err(Error::CouldNotVerifyDleq),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<Result<Vec<ProofInfo>, _>>()?;
|
||||||
|
|
||||||
|
// 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<Amount, Error> {
|
||||||
|
Ok(Amount::from(
|
||||||
|
self.get_unspent_auth_proofs().await?.len() as u64
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
68
crates/cdk/src/wallet/auth/mod.rs
Normal file
68
crates/cdk/src/wallet/auth/mod.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
mod auth_connector;
|
||||||
|
mod auth_wallet;
|
||||||
|
|
||||||
|
pub use auth_connector::AuthMintConnector;
|
||||||
|
pub use auth_wallet::AuthWallet;
|
||||||
|
use cdk_common::{Amount, AuthProof, AuthToken, Proofs};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use super::Wallet;
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
impl Wallet {
|
||||||
|
/// Mint blind auth tokens
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, Error> {
|
||||||
|
self.auth_wallet
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::AuthSettingsUndefined)?
|
||||||
|
.mint_blind_auth(amount)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get unspent auth proofs
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Error> {
|
||||||
|
self.auth_wallet
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::AuthSettingsUndefined)?
|
||||||
|
.get_unspent_auth_proofs()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set Clear Auth Token (CAT) for authentication
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn set_cat(&self, cat: String) -> Result<(), Error> {
|
||||||
|
let auth_wallet = self.auth_wallet.read().await;
|
||||||
|
if let Some(auth_wallet) = auth_wallet.as_ref() {
|
||||||
|
auth_wallet
|
||||||
|
.set_auth_token(AuthToken::ClearAuth(cat))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set refresh for authentication
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Error> {
|
||||||
|
let auth_wallet = self.auth_wallet.read().await;
|
||||||
|
if let Some(auth_wallet) = auth_wallet.as_ref() {
|
||||||
|
auth_wallet.set_refresh_token(Some(refresh_token)).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh CAT token
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn refresh_access_token(&self) -> Result<(), Error> {
|
||||||
|
let auth_wallet = self.auth_wallet.read().await;
|
||||||
|
if let Some(auth_wallet) = auth_wallet.as_ref() {
|
||||||
|
auth_wallet.refresh_access_token().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
161
crates/cdk/src/wallet/builder.rs
Normal file
161
crates/cdk/src/wallet/builder.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use bitcoin::bip32::Xpriv;
|
||||||
|
use bitcoin::Network;
|
||||||
|
use cdk_common::database;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use cdk_common::AuthToken;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::cdk_database::WalletDatabase;
|
||||||
|
use crate::error::Error;
|
||||||
|
use crate::mint_url::MintUrl;
|
||||||
|
use crate::nuts::CurrencyUnit;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::wallet::auth::AuthWallet;
|
||||||
|
use crate::wallet::{HttpClient, MintConnector, SubscriptionManager, Wallet};
|
||||||
|
|
||||||
|
/// Builder for creating a new [`Wallet`]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WalletBuilder {
|
||||||
|
mint_url: Option<MintUrl>,
|
||||||
|
unit: Option<CurrencyUnit>,
|
||||||
|
localstore: Option<Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>>,
|
||||||
|
target_proof_count: Option<usize>,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
auth_wallet: Option<AuthWallet>,
|
||||||
|
seed: Option<Vec<u8>>,
|
||||||
|
client: Option<Arc<dyn MintConnector + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WalletBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mint_url: None,
|
||||||
|
unit: None,
|
||||||
|
localstore: None,
|
||||||
|
target_proof_count: Some(3),
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
auth_wallet: None,
|
||||||
|
seed: None,
|
||||||
|
client: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletBuilder {
|
||||||
|
/// Create a new WalletBuilder
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the mint URL
|
||||||
|
pub fn mint_url(mut self, mint_url: MintUrl) -> Self {
|
||||||
|
self.mint_url = Some(mint_url);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the currency unit
|
||||||
|
pub fn unit(mut self, unit: CurrencyUnit) -> Self {
|
||||||
|
self.unit = Some(unit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the local storage backend
|
||||||
|
pub fn localstore(
|
||||||
|
mut self,
|
||||||
|
localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
|
||||||
|
) -> Self {
|
||||||
|
self.localstore = Some(localstore);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the target proof count
|
||||||
|
pub fn target_proof_count(mut self, count: usize) -> Self {
|
||||||
|
self.target_proof_count = Some(count);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the auth wallet
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub fn auth_wallet(mut self, auth_wallet: AuthWallet) -> Self {
|
||||||
|
self.auth_wallet = Some(auth_wallet);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the seed bytes
|
||||||
|
pub fn seed(mut self, seed: &[u8]) -> Self {
|
||||||
|
self.seed = Some(seed.to_vec());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a custom client connector
|
||||||
|
pub fn client<C: MintConnector + 'static + Send + Sync>(mut self, client: C) -> Self {
|
||||||
|
self.client = Some(Arc::new(client));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set auth CAT (Clear Auth Token)
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub fn set_auth_cat(mut self, cat: String) -> Self {
|
||||||
|
self.auth_wallet = Some(AuthWallet::new(
|
||||||
|
self.mint_url.clone().expect("Mint URL required"),
|
||||||
|
Some(AuthToken::ClearAuth(cat)),
|
||||||
|
self.localstore.clone().expect("Localstore required"),
|
||||||
|
HashMap::new(),
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the wallet
|
||||||
|
pub fn build(self) -> Result<Wallet, Error> {
|
||||||
|
let mint_url = self
|
||||||
|
.mint_url
|
||||||
|
.ok_or(Error::Custom("Mint url required".to_string()))?;
|
||||||
|
let unit = self
|
||||||
|
.unit
|
||||||
|
.ok_or(Error::Custom("Unit required".to_string()))?;
|
||||||
|
let localstore = self
|
||||||
|
.localstore
|
||||||
|
.ok_or(Error::Custom("Localstore required".to_string()))?;
|
||||||
|
let seed = self
|
||||||
|
.seed
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::Custom("Seed required".to_string()))?;
|
||||||
|
|
||||||
|
let xpriv = Xpriv::new_master(Network::Bitcoin, seed)?;
|
||||||
|
|
||||||
|
let client = match self.client {
|
||||||
|
Some(client) => client,
|
||||||
|
None => {
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
Arc::new(HttpClient::new(mint_url.clone(), self.auth_wallet.clone()))
|
||||||
|
as Arc<dyn MintConnector + Send + Sync>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
{
|
||||||
|
Arc::new(HttpClient::new(mint_url.clone()))
|
||||||
|
as Arc<dyn MintConnector + Send + Sync>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Wallet {
|
||||||
|
mint_url,
|
||||||
|
unit,
|
||||||
|
localstore,
|
||||||
|
target_proof_count: self.target_proof_count.unwrap_or(3),
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
auth_wallet: Arc::new(RwLock::new(self.auth_wallet)),
|
||||||
|
xpriv,
|
||||||
|
client: client.clone(),
|
||||||
|
subscription: SubscriptionManager::new(client),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
//! Wallet client
|
|
||||||
|
|
||||||
use std::fmt::Debug;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use reqwest::{Client, IntoUrl};
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::Serialize;
|
|
||||||
use tracing::instrument;
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use super::Error;
|
|
||||||
use crate::error::ErrorResponse;
|
|
||||||
use crate::mint_url::MintUrl;
|
|
||||||
use crate::nuts::{
|
|
||||||
CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse,
|
|
||||||
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
|
|
||||||
MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest,
|
|
||||||
RestoreResponse, SwapRequest, SwapResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Http Client
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct HttpClient {
|
|
||||||
inner: Client,
|
|
||||||
mint_url: MintUrl,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HttpClient {
|
|
||||||
/// Create new [`HttpClient`]
|
|
||||||
pub fn new(mint_url: MintUrl) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Client::new(),
|
|
||||||
mint_url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn http_get<U: IntoUrl, R: DeserializeOwned>(&self, url: U) -> Result<R, Error> {
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::HttpError(e.to_string()))?
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::HttpError(e.to_string()))?;
|
|
||||||
|
|
||||||
serde_json::from_str::<R>(&response).map_err(|err| {
|
|
||||||
tracing::warn!("Http Response error: {}", err);
|
|
||||||
match ErrorResponse::from_json(&response) {
|
|
||||||
Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
|
|
||||||
Err(err) => err.into(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
async fn http_post<U: IntoUrl, P: Serialize + ?Sized, R: DeserializeOwned>(
|
|
||||||
&self,
|
|
||||||
url: U,
|
|
||||||
payload: &P,
|
|
||||||
) -> Result<R, Error> {
|
|
||||||
let response = self
|
|
||||||
.inner
|
|
||||||
.post(url)
|
|
||||||
.json(&payload)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::HttpError(e.to_string()))?
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::HttpError(e.to_string()))?;
|
|
||||||
|
|
||||||
serde_json::from_str::<R>(&response).map_err(|err| {
|
|
||||||
tracing::warn!("Http Response error: {}", err);
|
|
||||||
match ErrorResponse::from_json(&response) {
|
|
||||||
Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
|
|
||||||
Err(err) => err.into(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
/// Create new [`HttpClient`] with a proxy for specific TLDs.
|
|
||||||
/// Specifying `None` for `host_matcher` will use the proxy for all
|
|
||||||
/// requests.
|
|
||||||
pub fn with_proxy(
|
|
||||||
mint_url: MintUrl,
|
|
||||||
proxy: Url,
|
|
||||||
host_matcher: Option<&str>,
|
|
||||||
accept_invalid_certs: bool,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let regex = host_matcher
|
|
||||||
.map(regex::Regex::new)
|
|
||||||
.transpose()
|
|
||||||
.map_err(|e| Error::Custom(e.to_string()))?;
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.proxy(reqwest::Proxy::custom(move |url| {
|
|
||||||
if let Some(matcher) = regex.as_ref() {
|
|
||||||
if let Some(host) = url.host_str() {
|
|
||||||
if matcher.is_match(host) {
|
|
||||||
return Some(proxy.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}))
|
|
||||||
.danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
|
|
||||||
.build()
|
|
||||||
.map_err(|e| Error::HttpError(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
inner: client,
|
|
||||||
mint_url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
|
||||||
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<Vec<KeySet>, Error> {
|
|
||||||
let url = self.mint_url.join_paths(&["v1", "keys"])?;
|
|
||||||
Ok(self.http_get::<_, KeysResponse>(url).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<KeySet, Error> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get Keysets [NUT-02]
|
|
||||||
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error> {
|
|
||||||
let url = self.mint_url.join_paths(&["v1", "keysets"])?;
|
|
||||||
self.http_get(url).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mint Quote [NUT-04]
|
|
||||||
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn post_mint_quote(
|
|
||||||
&self,
|
|
||||||
request: MintQuoteBolt11Request,
|
|
||||||
) -> Result<MintQuoteBolt11Response<String>, Error> {
|
|
||||||
let url = self
|
|
||||||
.mint_url
|
|
||||||
.join_paths(&["v1", "mint", "quote", "bolt11"])?;
|
|
||||||
self.http_post(url, &request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mint Quote status
|
|
||||||
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn get_mint_quote_status(
|
|
||||||
&self,
|
|
||||||
quote_id: &str,
|
|
||||||
) -> Result<MintQuoteBolt11Response<String>, Error> {
|
|
||||||
let url = self
|
|
||||||
.mint_url
|
|
||||||
.join_paths(&["v1", "mint", "quote", "bolt11", quote_id])?;
|
|
||||||
|
|
||||||
self.http_get(url).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mint Tokens [NUT-04]
|
|
||||||
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn post_mint(
|
|
||||||
&self,
|
|
||||||
request: MintBolt11Request<String>,
|
|
||||||
) -> Result<MintBolt11Response, Error> {
|
|
||||||
let url = self.mint_url.join_paths(&["v1", "mint", "bolt11"])?;
|
|
||||||
self.http_post(url, &request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Melt Quote [NUT-05]
|
|
||||||
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn post_melt_quote(
|
|
||||||
&self,
|
|
||||||
request: MeltQuoteBolt11Request,
|
|
||||||
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
|
||||||
let url = self
|
|
||||||
.mint_url
|
|
||||||
.join_paths(&["v1", "melt", "quote", "bolt11"])?;
|
|
||||||
self.http_post(url, &request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Melt Quote Status
|
|
||||||
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn get_melt_quote_status(
|
|
||||||
&self,
|
|
||||||
quote_id: &str,
|
|
||||||
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
|
||||||
let url = self
|
|
||||||
.mint_url
|
|
||||||
.join_paths(&["v1", "melt", "quote", "bolt11", quote_id])?;
|
|
||||||
|
|
||||||
self.http_get(url).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Melt [NUT-05]
|
|
||||||
/// [Nut-08] Lightning fee return if outputs defined
|
|
||||||
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn post_melt(
|
|
||||||
&self,
|
|
||||||
request: MeltBolt11Request<String>,
|
|
||||||
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
|
||||||
let url = self.mint_url.join_paths(&["v1", "melt", "bolt11"])?;
|
|
||||||
self.http_post(url, &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<SwapResponse, Error> {
|
|
||||||
let url = self.mint_url.join_paths(&["v1", "swap"])?;
|
|
||||||
self.http_post(url, &swap_request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get Mint Info [NUT-06]
|
|
||||||
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
|
|
||||||
let url = self.mint_url.join_paths(&["v1", "info"])?;
|
|
||||||
self.http_get(url).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spendable check [NUT-07]
|
|
||||||
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn post_check_state(
|
|
||||||
&self,
|
|
||||||
request: CheckStateRequest,
|
|
||||||
) -> Result<CheckStateResponse, Error> {
|
|
||||||
let url = self.mint_url.join_paths(&["v1", "checkstate"])?;
|
|
||||||
self.http_post(url, &request).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restore request [NUT-13]
|
|
||||||
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
|
||||||
async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
|
|
||||||
let url = self.mint_url.join_paths(&["v1", "restore"])?;
|
|
||||||
self.http_post(url, &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<Vec<KeySet>, Error>;
|
|
||||||
/// Get Keyset Keys [NUT-01]
|
|
||||||
async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error>;
|
|
||||||
/// Get Keysets [NUT-02]
|
|
||||||
async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error>;
|
|
||||||
/// Mint Quote [NUT-04]
|
|
||||||
async fn post_mint_quote(
|
|
||||||
&self,
|
|
||||||
request: MintQuoteBolt11Request,
|
|
||||||
) -> Result<MintQuoteBolt11Response<String>, Error>;
|
|
||||||
/// Mint Quote status
|
|
||||||
async fn get_mint_quote_status(
|
|
||||||
&self,
|
|
||||||
quote_id: &str,
|
|
||||||
) -> Result<MintQuoteBolt11Response<String>, Error>;
|
|
||||||
/// Mint Tokens [NUT-04]
|
|
||||||
async fn post_mint(
|
|
||||||
&self,
|
|
||||||
request: MintBolt11Request<String>,
|
|
||||||
) -> Result<MintBolt11Response, Error>;
|
|
||||||
/// Melt Quote [NUT-05]
|
|
||||||
async fn post_melt_quote(
|
|
||||||
&self,
|
|
||||||
request: MeltQuoteBolt11Request,
|
|
||||||
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
|
||||||
/// Melt Quote Status
|
|
||||||
async fn get_melt_quote_status(
|
|
||||||
&self,
|
|
||||||
quote_id: &str,
|
|
||||||
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
|
||||||
/// Melt [NUT-05]
|
|
||||||
/// [Nut-08] Lightning fee return if outputs defined
|
|
||||||
async fn post_melt(
|
|
||||||
&self,
|
|
||||||
request: MeltBolt11Request<String>,
|
|
||||||
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
|
||||||
/// Split Token [NUT-06]
|
|
||||||
async fn post_swap(&self, request: SwapRequest) -> Result<SwapResponse, Error>;
|
|
||||||
/// Get Mint Info [NUT-06]
|
|
||||||
async fn get_mint_info(&self) -> Result<MintInfo, Error>;
|
|
||||||
/// Spendable check [NUT-07]
|
|
||||||
async fn post_check_state(
|
|
||||||
&self,
|
|
||||||
request: CheckStateRequest,
|
|
||||||
) -> Result<CheckStateResponse, Error>;
|
|
||||||
/// Restore request [NUT-13]
|
|
||||||
async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error>;
|
|
||||||
}
|
|
||||||
480
crates/cdk/src/wallet/mint_connector/http_client.rs
Normal file
480
crates/cdk/src/wallet/mint_connector/http_client.rs
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use cdk_common::{Method, ProtectedEndpoint, RoutePath};
|
||||||
|
use reqwest::{Client, IntoUrl};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tracing::instrument;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use super::{Error, MintConnector};
|
||||||
|
use crate::error::ErrorResponse;
|
||||||
|
use crate::mint_url::MintUrl;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::nuts::nut22::MintAuthRequest;
|
||||||
|
use crate::nuts::{
|
||||||
|
AuthToken, CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse,
|
||||||
|
MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
|
||||||
|
MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest,
|
||||||
|
RestoreResponse, SwapRequest, SwapResponse,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::wallet::auth::{AuthMintConnector, AuthWallet};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct HttpClientCore {
|
||||||
|
inner: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpClientCore {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client(&self) -> &Client {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn http_get<U: IntoUrl + Send, R: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
url: U,
|
||||||
|
auth: Option<AuthToken>,
|
||||||
|
) -> Result<R, Error> {
|
||||||
|
let mut request = self.client().get(url);
|
||||||
|
|
||||||
|
if let Some(auth) = auth {
|
||||||
|
request = request.header(auth.header_key(), auth.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = request
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::HttpError(e.to_string()))?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::HttpError(e.to_string()))?;
|
||||||
|
|
||||||
|
serde_json::from_str::<R>(&response).map_err(|err| {
|
||||||
|
tracing::warn!("Http Response error: {}", err);
|
||||||
|
match ErrorResponse::from_json(&response) {
|
||||||
|
Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
|
||||||
|
Err(err) => err.into(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn http_post<U: IntoUrl + Send, P: Serialize + ?Sized, R: DeserializeOwned>(
|
||||||
|
&self,
|
||||||
|
url: U,
|
||||||
|
auth_token: Option<AuthToken>,
|
||||||
|
payload: &P,
|
||||||
|
) -> Result<R, Error> {
|
||||||
|
let mut request = self.client().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()))?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::HttpError(e.to_string()))?;
|
||||||
|
|
||||||
|
serde_json::from_str::<R>(&response).map_err(|err| {
|
||||||
|
tracing::warn!("Http Response error: {}", err);
|
||||||
|
match ErrorResponse::from_json(&response) {
|
||||||
|
Ok(ok) => <ErrorResponse as Into<Error>>::into(ok),
|
||||||
|
Err(err) => err.into(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Http Client
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HttpClient {
|
||||||
|
core: HttpClientCore,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpClient {
|
||||||
|
/// Create new [`HttpClient`]
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub fn new(mint_url: MintUrl, auth_wallet: Option<AuthWallet>) -> Self {
|
||||||
|
Self {
|
||||||
|
core: HttpClientCore::new(),
|
||||||
|
mint_url,
|
||||||
|
auth_wallet: Arc::new(RwLock::new(auth_wallet)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
/// Create new [`HttpClient`]
|
||||||
|
pub fn new(mint_url: MintUrl) -> Self {
|
||||||
|
Self {
|
||||||
|
core: HttpClientCore::new(),
|
||||||
|
mint_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get auth token for a protected endpoint
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
async fn get_auth_token(
|
||||||
|
&self,
|
||||||
|
method: Method,
|
||||||
|
path: RoutePath,
|
||||||
|
) -> Result<Option<AuthToken>, Error> {
|
||||||
|
let auth_wallet = self.auth_wallet.read().await;
|
||||||
|
match auth_wallet.as_ref() {
|
||||||
|
Some(auth_wallet) => {
|
||||||
|
let endpoint = ProtectedEndpoint::new(method, path);
|
||||||
|
auth_wallet.get_auth_for_request(&endpoint).await
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
/// Create new [`HttpClient`] with a proxy for specific TLDs.
|
||||||
|
/// Specifying `None` for `host_matcher` will use the proxy for all
|
||||||
|
/// requests.
|
||||||
|
pub fn with_proxy(
|
||||||
|
mint_url: MintUrl,
|
||||||
|
proxy: Url,
|
||||||
|
host_matcher: Option<&str>,
|
||||||
|
accept_invalid_certs: bool,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let regex = host_matcher
|
||||||
|
.map(regex::Regex::new)
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| Error::Custom(e.to_string()))?;
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.proxy(reqwest::Proxy::custom(move |url| {
|
||||||
|
if let Some(matcher) = regex.as_ref() {
|
||||||
|
if let Some(host) = url.host_str() {
|
||||||
|
if matcher.is_match(host) {
|
||||||
|
return Some(proxy.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}))
|
||||||
|
.danger_accept_invalid_certs(accept_invalid_certs) // Allow self-signed certs
|
||||||
|
.build()
|
||||||
|
.map_err(|e| Error::HttpError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
core: HttpClientCore { inner: client },
|
||||||
|
mint_url,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
auth_wallet: Arc::new(RwLock::new(None)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||||
|
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<Vec<KeySet>, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "keys"])?;
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.core
|
||||||
|
.http_get::<_, KeysResponse>(url, None)
|
||||||
|
.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<KeySet, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "keys", &keyset_id.to_string()])?;
|
||||||
|
|
||||||
|
let keys_response = self.core.http_get::<_, KeysResponse>(url, None).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<KeysetResponse, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "keysets"])?;
|
||||||
|
self.core.http_get(url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint Quote [NUT-04]
|
||||||
|
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_mint_quote(
|
||||||
|
&self,
|
||||||
|
request: MintQuoteBolt11Request,
|
||||||
|
) -> Result<MintQuoteBolt11Response<String>, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "mint", "quote", "bolt11"])?;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::MintQuoteBolt11)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
|
||||||
|
self.core.http_post(url, auth_token, &request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint Quote status
|
||||||
|
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn get_mint_quote_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MintQuoteBolt11Response<String>, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "mint", "quote", "bolt11", quote_id])?;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Get, RoutePath::MintQuoteBolt11)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_get(url, auth_token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint Tokens [NUT-04]
|
||||||
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_mint(
|
||||||
|
&self,
|
||||||
|
request: MintBolt11Request<String>,
|
||||||
|
) -> Result<MintBolt11Response, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "mint", "bolt11"])?;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::MintBolt11)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_post(url, auth_token, &request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Melt Quote [NUT-05]
|
||||||
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_melt_quote(
|
||||||
|
&self,
|
||||||
|
request: MeltQuoteBolt11Request,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "melt", "quote", "bolt11"])?;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::MeltQuoteBolt11)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_post(url, auth_token, &request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Melt Quote Status
|
||||||
|
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn get_melt_quote_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "melt", "quote", "bolt11", quote_id])?;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Get, RoutePath::MeltQuoteBolt11)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_get(url, auth_token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Melt [NUT-05]
|
||||||
|
/// [Nut-08] Lightning fee return if outputs defined
|
||||||
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_melt(
|
||||||
|
&self,
|
||||||
|
request: MeltBolt11Request<String>,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "melt", "bolt11"])?;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::MeltBolt11)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.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<SwapResponse, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "swap"])?;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self.get_auth_token(Method::Post, RoutePath::Swap).await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_post(url, auth_token, &swap_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to get mint info
|
||||||
|
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "info"])?;
|
||||||
|
self.core.http_get(url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
async fn get_auth_wallet(&self) -> Option<AuthWallet> {
|
||||||
|
self.auth_wallet.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
async fn set_auth_wallet(&self, wallet: Option<AuthWallet>) {
|
||||||
|
*self.auth_wallet.write().await = wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spendable check [NUT-07]
|
||||||
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_check_state(
|
||||||
|
&self,
|
||||||
|
request: CheckStateRequest,
|
||||||
|
) -> Result<CheckStateResponse, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "checkstate"])?;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::Checkstate)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.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<RestoreResponse, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "restore"])?;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
let auth_token = self
|
||||||
|
.get_auth_token(Method::Post, RoutePath::Restore)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "auth"))]
|
||||||
|
let auth_token = None;
|
||||||
|
self.core.http_post(url, auth_token, &request).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Http Client
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub struct AuthHttpClient {
|
||||||
|
core: HttpClientCore,
|
||||||
|
mint_url: MintUrl,
|
||||||
|
cat: Arc<RwLock<AuthToken>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
impl AuthHttpClient {
|
||||||
|
/// Create new [`AuthHttpClient`]
|
||||||
|
pub fn new(mint_url: MintUrl, cat: Option<AuthToken>) -> Self {
|
||||||
|
Self {
|
||||||
|
core: HttpClientCore::new(),
|
||||||
|
mint_url,
|
||||||
|
cat: Arc::new(RwLock::new(
|
||||||
|
cat.unwrap_or(AuthToken::ClearAuth("".to_string())),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
impl AuthMintConnector for AuthHttpClient {
|
||||||
|
async fn get_auth_token(&self) -> Result<AuthToken, Error> {
|
||||||
|
Ok(self.cat.read().await.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_auth_token(&self, token: AuthToken) -> Result<(), Error> {
|
||||||
|
*self.cat.write().await = token;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Mint Info [NUT-06]
|
||||||
|
async fn get_mint_info(&self) -> Result<MintInfo, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "info"])?;
|
||||||
|
let mint_info: MintInfo = self.core.http_get::<_, MintInfo>(url, None).await?;
|
||||||
|
|
||||||
|
Ok(mint_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Auth Keyset Keys [NUT-22]
|
||||||
|
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn get_mint_blind_auth_keyset(&self, keyset_id: Id) -> Result<KeySet, Error> {
|
||||||
|
let url =
|
||||||
|
self.mint_url
|
||||||
|
.join_paths(&["v1", "auth", "blind", "keys", &keyset_id.to_string()])?;
|
||||||
|
|
||||||
|
let mut keys_response = self.core.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-22]
|
||||||
|
#[instrument(skip(self), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn get_mint_blind_auth_keysets(&self) -> Result<KeysetResponse, Error> {
|
||||||
|
let url = self
|
||||||
|
.mint_url
|
||||||
|
.join_paths(&["v1", "auth", "blind", "keysets"])?;
|
||||||
|
|
||||||
|
self.core.http_get(url, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mint Tokens [NUT-22]
|
||||||
|
#[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
|
||||||
|
async fn post_mint_blind_auth(
|
||||||
|
&self,
|
||||||
|
request: MintAuthRequest,
|
||||||
|
) -> Result<MintBolt11Response, Error> {
|
||||||
|
let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?;
|
||||||
|
self.core
|
||||||
|
.http_post(url, Some(self.cat.read().await.clone()), &request)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
83
crates/cdk/src/wallet/mint_connector/mod.rs
Normal file
83
crates/cdk/src/wallet/mint_connector/mod.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
//! Wallet client
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use super::Error;
|
||||||
|
use crate::nuts::{
|
||||||
|
CheckStateRequest, CheckStateResponse, Id, KeySet, KeysetResponse, MeltBolt11Request,
|
||||||
|
MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
|
||||||
|
MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse,
|
||||||
|
SwapRequest, SwapResponse,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::wallet::AuthWallet;
|
||||||
|
|
||||||
|
mod http_client;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use http_client::AuthHttpClient;
|
||||||
|
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) -> Result<Vec<KeySet>, Error>;
|
||||||
|
/// Get Keyset Keys [NUT-01]
|
||||||
|
async fn get_mint_keyset(&self, keyset_id: Id) -> Result<KeySet, Error>;
|
||||||
|
/// Get Keysets [NUT-02]
|
||||||
|
async fn get_mint_keysets(&self) -> Result<KeysetResponse, Error>;
|
||||||
|
/// Mint Quote [NUT-04]
|
||||||
|
async fn post_mint_quote(
|
||||||
|
&self,
|
||||||
|
request: MintQuoteBolt11Request,
|
||||||
|
) -> Result<MintQuoteBolt11Response<String>, Error>;
|
||||||
|
/// Mint Quote status
|
||||||
|
async fn get_mint_quote_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MintQuoteBolt11Response<String>, Error>;
|
||||||
|
/// Mint Tokens [NUT-04]
|
||||||
|
async fn post_mint(
|
||||||
|
&self,
|
||||||
|
request: MintBolt11Request<String>,
|
||||||
|
) -> Result<MintBolt11Response, Error>;
|
||||||
|
/// Melt Quote [NUT-05]
|
||||||
|
async fn post_melt_quote(
|
||||||
|
&self,
|
||||||
|
request: MeltQuoteBolt11Request,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
||||||
|
/// Melt Quote Status
|
||||||
|
async fn get_melt_quote_status(
|
||||||
|
&self,
|
||||||
|
quote_id: &str,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
||||||
|
/// Melt [NUT-05]
|
||||||
|
/// [Nut-08] Lightning fee return if outputs defined
|
||||||
|
async fn post_melt(
|
||||||
|
&self,
|
||||||
|
request: MeltBolt11Request<String>,
|
||||||
|
) -> Result<MeltQuoteBolt11Response<String>, Error>;
|
||||||
|
/// Split Token [NUT-06]
|
||||||
|
async fn post_swap(&self, request: SwapRequest) -> Result<SwapResponse, Error>;
|
||||||
|
/// Get Mint Info [NUT-06]
|
||||||
|
async fn get_mint_info(&self) -> Result<MintInfo, Error>;
|
||||||
|
/// Spendable check [NUT-07]
|
||||||
|
async fn post_check_state(
|
||||||
|
&self,
|
||||||
|
request: CheckStateRequest,
|
||||||
|
) -> Result<CheckStateResponse, Error>;
|
||||||
|
/// Restore request [NUT-13]
|
||||||
|
async fn post_restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error>;
|
||||||
|
|
||||||
|
/// Get the auth wallet for the client
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
async fn get_auth_wallet(&self) -> Option<AuthWallet>;
|
||||||
|
|
||||||
|
/// Set auth wallet on client
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
async fn set_auth_wallet(&self, wallet: Option<AuthWallet>);
|
||||||
|
}
|
||||||
@@ -5,15 +5,13 @@ use std::str::FromStr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bitcoin::bip32::Xpriv;
|
use bitcoin::bip32::Xpriv;
|
||||||
use bitcoin::Network;
|
|
||||||
use cdk_common::database::{self, WalletDatabase};
|
use cdk_common::database::{self, WalletDatabase};
|
||||||
use cdk_common::subscription::Params;
|
use cdk_common::subscription::Params;
|
||||||
use client::MintConnector;
|
|
||||||
use getrandom::getrandom;
|
use getrandom::getrandom;
|
||||||
pub use multi_mint_wallet::MultiMintWallet;
|
|
||||||
use subscription::{ActiveSubscription, SubscriptionManager};
|
use subscription::{ActiveSubscription, SubscriptionManager};
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use tokio::sync::RwLock;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
pub use types::{MeltQuote, MintQuote, SendKind};
|
|
||||||
|
|
||||||
use crate::amount::SplitTarget;
|
use crate::amount::SplitTarget;
|
||||||
use crate::dhke::construct_proofs;
|
use crate::dhke::construct_proofs;
|
||||||
@@ -27,13 +25,19 @@ use crate::nuts::{
|
|||||||
RestoreRequest, SpendingConditions, State,
|
RestoreRequest, SpendingConditions, State,
|
||||||
};
|
};
|
||||||
use crate::types::ProofInfo;
|
use crate::types::ProofInfo;
|
||||||
use crate::{Amount, HttpClient};
|
use crate::util::unix_time;
|
||||||
|
use crate::Amount;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
use crate::OidcClient;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
mod auth;
|
||||||
mod balance;
|
mod balance;
|
||||||
pub mod client;
|
mod builder;
|
||||||
mod keysets;
|
mod keysets;
|
||||||
mod melt;
|
mod melt;
|
||||||
mod mint;
|
mod mint;
|
||||||
|
mod mint_connector;
|
||||||
pub mod multi_mint_wallet;
|
pub mod multi_mint_wallet;
|
||||||
mod proofs;
|
mod proofs;
|
||||||
mod receive;
|
mod receive;
|
||||||
@@ -42,8 +46,16 @@ pub mod subscription;
|
|||||||
mod swap;
|
mod swap;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use auth::{AuthMintConnector, AuthWallet};
|
||||||
|
pub use builder::WalletBuilder;
|
||||||
pub use cdk_common::wallet as types;
|
pub use cdk_common::wallet as types;
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
pub use mint_connector::AuthHttpClient;
|
||||||
|
pub use mint_connector::{HttpClient, MintConnector};
|
||||||
|
pub use multi_mint_wallet::MultiMintWallet;
|
||||||
pub use send::{PreparedSend, SendMemo, SendOptions};
|
pub use send::{PreparedSend, SendMemo, SendOptions};
|
||||||
|
pub use types::{MeltQuote, MintQuote, SendKind};
|
||||||
|
|
||||||
use crate::nuts::nut00::ProofsMethods;
|
use crate::nuts::nut00::ProofsMethods;
|
||||||
|
|
||||||
@@ -62,6 +74,8 @@ pub struct Wallet {
|
|||||||
pub localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
|
pub localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
|
||||||
/// The targeted amount of proofs to have at each size
|
/// The targeted amount of proofs to have at each size
|
||||||
pub target_proof_count: usize,
|
pub target_proof_count: usize,
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
|
||||||
xpriv: Xpriv,
|
xpriv: Xpriv,
|
||||||
client: Arc<dyn MintConnector + Send + Sync>,
|
client: Arc<dyn MintConnector + Send + Sync>,
|
||||||
subscription: SubscriptionManager,
|
subscription: SubscriptionManager,
|
||||||
@@ -115,14 +129,16 @@ impl From<WalletSubscription> for Params {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Wallet {
|
impl Wallet {
|
||||||
/// Create new [`Wallet`]
|
/// Create new [`Wallet`] using the builder pattern
|
||||||
/// # Synopsis
|
/// # Synopsis
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use std::sync::Arc;
|
/// use std::sync::Arc;
|
||||||
|
/// use bitcoin::Network;
|
||||||
|
/// use bitcoin::bip32::Xpriv;
|
||||||
///
|
///
|
||||||
/// use cdk_sqlite::wallet::memory;
|
/// use cdk_sqlite::wallet::memory;
|
||||||
/// use cdk::nuts::CurrencyUnit;
|
/// use cdk::nuts::CurrencyUnit;
|
||||||
/// use cdk::wallet::Wallet;
|
/// use cdk::wallet::{Wallet, WalletBuilder};
|
||||||
/// use rand::Rng;
|
/// use rand::Rng;
|
||||||
///
|
///
|
||||||
/// async fn test() -> anyhow::Result<()> {
|
/// async fn test() -> anyhow::Result<()> {
|
||||||
@@ -131,7 +147,12 @@ impl Wallet {
|
|||||||
/// let unit = CurrencyUnit::Sat;
|
/// let unit = CurrencyUnit::Sat;
|
||||||
///
|
///
|
||||||
/// let localstore = memory::empty().await?;
|
/// let localstore = memory::empty().await?;
|
||||||
/// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
|
/// let wallet = WalletBuilder::new()
|
||||||
|
/// .mint_url(mint_url.parse().unwrap())
|
||||||
|
/// .unit(unit)
|
||||||
|
/// .localstore(Arc::new(localstore))
|
||||||
|
/// .seed(&seed)
|
||||||
|
/// .build();
|
||||||
/// Ok(())
|
/// Ok(())
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
@@ -142,32 +163,21 @@ impl Wallet {
|
|||||||
seed: &[u8],
|
seed: &[u8],
|
||||||
target_proof_count: Option<usize>,
|
target_proof_count: Option<usize>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let xpriv = Xpriv::new_master(Network::Bitcoin, seed).expect("Could not create master key");
|
|
||||||
let mint_url = MintUrl::from_str(mint_url)?;
|
let mint_url = MintUrl::from_str(mint_url)?;
|
||||||
|
|
||||||
let http_client = Arc::new(HttpClient::new(mint_url.clone()));
|
WalletBuilder::new()
|
||||||
|
.mint_url(mint_url)
|
||||||
Ok(Self {
|
.unit(unit)
|
||||||
mint_url: mint_url.clone(),
|
.localstore(localstore)
|
||||||
unit,
|
.seed(seed)
|
||||||
client: http_client.clone(),
|
.target_proof_count(target_proof_count.unwrap_or(3))
|
||||||
subscription: SubscriptionManager::new(http_client),
|
.build()
|
||||||
localstore,
|
|
||||||
xpriv,
|
|
||||||
target_proof_count: target_proof_count.unwrap_or(3),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change HTTP client
|
|
||||||
pub fn set_client<C: MintConnector + 'static + Send + Sync>(&mut self, client: C) {
|
|
||||||
self.client = Arc::new(client);
|
|
||||||
self.subscription = SubscriptionManager::new(self.client.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe to events
|
/// Subscribe to events
|
||||||
pub async fn subscribe<T: Into<Params>>(&self, query: T) -> ActiveSubscription {
|
pub async fn subscribe<T: Into<Params>>(&self, query: T) -> ActiveSubscription {
|
||||||
self.subscription
|
self.subscription
|
||||||
.subscribe(self.mint_url.clone(), query.into())
|
.subscribe(self.mint_url.clone(), query.into(), Arc::new(self.clone()))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,21 +242,68 @@ impl Wallet {
|
|||||||
/// Query mint for current mint information
|
/// Query mint for current mint information
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, Error> {
|
pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, Error> {
|
||||||
let mint_info = match self.client.get_mint_info().await {
|
match self.client.get_mint_info().await {
|
||||||
Ok(mint_info) => Some(mint_info),
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update auth wallet
|
||||||
|
#[cfg(feature = "auth")]
|
||||||
|
{
|
||||||
|
let mut auth_wallet = self.auth_wallet.write().await;
|
||||||
|
match &*auth_wallet {
|
||||||
|
Some(auth_wallet) => {
|
||||||
|
let mut protected_endpoints =
|
||||||
|
auth_wallet.protected_endpoints.write().await;
|
||||||
|
*protected_endpoints = mint_info.protected_endpoints();
|
||||||
|
|
||||||
|
if let Some(oidc_client) =
|
||||||
|
mint_info.openid_discovery().map(OidcClient::new)
|
||||||
|
{
|
||||||
|
auth_wallet.set_oidc_client(Some(oidc_client)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::info!("Mint has auth enabled creating auth wallet");
|
||||||
|
|
||||||
|
let oidc_client = mint_info.openid_discovery().map(OidcClient::new);
|
||||||
|
let new_auth_wallet = AuthWallet::new(
|
||||||
|
self.mint_url.clone(),
|
||||||
|
None,
|
||||||
|
self.localstore.clone(),
|
||||||
|
mint_info.protected_endpoints(),
|
||||||
|
oidc_client,
|
||||||
|
);
|
||||||
|
*auth_wallet = Some(new_auth_wallet.clone());
|
||||||
|
|
||||||
|
self.client.set_auth_wallet(Some(new_auth_wallet)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.localstore
|
||||||
|
.add_mint(self.mint_url.clone(), Some(mint_info.clone()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tracing::trace!("Mint info updated for {}", self.mint_url);
|
||||||
|
|
||||||
|
Ok(Some(mint_info))
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::warn!("Could not get mint info {}", err);
|
tracing::warn!("Could not get mint info {}", err);
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
self.localstore
|
|
||||||
.add_mint(self.mint_url.clone(), mint_info.clone())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
tracing::trace!("Mint info updated for {}", self.mint_url);
|
|
||||||
|
|
||||||
Ok(mint_info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get amounts needed to refill proof state
|
/// Get amounts needed to refill proof state
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ impl Wallet {
|
|||||||
.client
|
.client
|
||||||
.post_check_state(CheckStateRequest { ys: proofs.ys()? })
|
.post_check_state(CheckStateRequest { ys: proofs.ys()? })
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let spent_ys: Vec<_> = spendable
|
let spent_ys: Vec<_> = spendable
|
||||||
.states
|
.states
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ use super::WsSubscriptionBody;
|
|||||||
use crate::nuts::nut17::Kind;
|
use crate::nuts::nut17::Kind;
|
||||||
use crate::nuts::{nut01, nut04, nut05, nut07, CheckStateRequest, NotificationPayload};
|
use crate::nuts::{nut01, nut04, nut05, nut07, CheckStateRequest, NotificationPayload};
|
||||||
use crate::pub_sub::SubId;
|
use crate::pub_sub::SubId;
|
||||||
use crate::wallet::client::MintConnector;
|
use crate::wallet::MintConnector;
|
||||||
|
use crate::Wallet;
|
||||||
|
|
||||||
#[derive(Debug, Hash, PartialEq, Eq)]
|
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||||
enum UrlType {
|
enum UrlType {
|
||||||
@@ -77,6 +78,7 @@ pub async fn http_main<S: IntoIterator<Item = SubId>>(
|
|||||||
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
||||||
mut new_subscription_recv: mpsc::Receiver<SubId>,
|
mut new_subscription_recv: mpsc::Receiver<SubId>,
|
||||||
mut on_drop: mpsc::Receiver<SubId>,
|
mut on_drop: mpsc::Receiver<SubId>,
|
||||||
|
_wallet: Arc<Wallet>,
|
||||||
) {
|
) {
|
||||||
let mut interval = time::interval(Duration::from_secs(2));
|
let mut interval = time::interval(Duration::from_secs(2));
|
||||||
let mut subscribed_to = HashMap::<UrlType, (mpsc::Sender<_>, _, AnyState)>::new();
|
let mut subscribed_to = HashMap::<UrlType, (mpsc::Sender<_>, _, AnyState)>::new();
|
||||||
@@ -92,6 +94,7 @@ pub async fn http_main<S: IntoIterator<Item = SubId>>(
|
|||||||
tracing::debug!("Polling: {:?}", url);
|
tracing::debug!("Polling: {:?}", url);
|
||||||
match url {
|
match url {
|
||||||
UrlType::Mint(id) => {
|
UrlType::Mint(id) => {
|
||||||
|
|
||||||
let response = http_client.get_mint_quote_status(id).await;
|
let response = http_client.get_mint_quote_status(id).await;
|
||||||
if let Ok(response) = response {
|
if let Ok(response) = response {
|
||||||
if *last_state == AnyState::MintQuoteState(response.state) {
|
if *last_state == AnyState::MintQuoteState(response.state) {
|
||||||
@@ -104,6 +107,7 @@ pub async fn http_main<S: IntoIterator<Item = SubId>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
UrlType::Melt(id) => {
|
UrlType::Melt(id) => {
|
||||||
|
|
||||||
let response = http_client.get_melt_quote_status(id).await;
|
let response = http_client.get_melt_quote_status(id).await;
|
||||||
if let Ok(response) = response {
|
if let Ok(response) = response {
|
||||||
if *last_state == AnyState::MeltQuoteState(response.state) {
|
if *last_state == AnyState::MeltQuoteState(response.state) {
|
||||||
@@ -118,7 +122,8 @@ pub async fn http_main<S: IntoIterator<Item = SubId>>(
|
|||||||
UrlType::PublicKey(id) => {
|
UrlType::PublicKey(id) => {
|
||||||
let responses = http_client.post_check_state(CheckStateRequest {
|
let responses = http_client.post_check_state(CheckStateRequest {
|
||||||
ys: vec![*id],
|
ys: vec![*id],
|
||||||
}).await;
|
}
|
||||||
|
).await;
|
||||||
if let Ok(mut responses) = responses {
|
if let Ok(mut responses) = responses {
|
||||||
let response = if let Some(state) = responses.states.pop() {
|
let response = if let Some(state) = responses.states.pop() {
|
||||||
state
|
state
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ use tokio::sync::{mpsc, RwLock};
|
|||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
use super::Wallet;
|
||||||
use crate::mint_url::MintUrl;
|
use crate::mint_url::MintUrl;
|
||||||
use crate::pub_sub::SubId;
|
use crate::pub_sub::SubId;
|
||||||
use crate::wallet::client::MintConnector;
|
use crate::wallet::MintConnector;
|
||||||
|
|
||||||
mod http;
|
mod http;
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
@@ -59,7 +60,12 @@ impl SubscriptionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe to updates from a mint server with a given filter
|
/// 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<Wallet>,
|
||||||
|
) -> ActiveSubscription {
|
||||||
let subscription_clients = self.all_connections.read().await;
|
let subscription_clients = self.all_connections.read().await;
|
||||||
let id = filter.id.clone();
|
let id = filter.id.clone();
|
||||||
if let Some(subscription_client) = subscription_clients.get(&mint_url) {
|
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 mut subscription_clients = self.all_connections.write().await;
|
||||||
let subscription_client =
|
let subscription_client = SubscriptionClient::new(
|
||||||
SubscriptionClient::new(mint_url.clone(), self.http_client.clone(), is_ws_support);
|
mint_url.clone(),
|
||||||
|
self.http_client.clone(),
|
||||||
|
is_ws_support,
|
||||||
|
wallet,
|
||||||
|
);
|
||||||
let (on_drop_notif, receiver) = subscription_client.subscribe(filter).await;
|
let (on_drop_notif, receiver) = subscription_client.subscribe(filter).await;
|
||||||
subscription_clients.insert(mint_url, subscription_client);
|
subscription_clients.insert(mint_url, subscription_client);
|
||||||
|
|
||||||
@@ -179,6 +189,7 @@ impl SubscriptionClient {
|
|||||||
url: MintUrl,
|
url: MintUrl,
|
||||||
http_client: Arc<dyn MintConnector + Send + Sync>,
|
http_client: Arc<dyn MintConnector + Send + Sync>,
|
||||||
prefer_ws_method: bool,
|
prefer_ws_method: bool,
|
||||||
|
wallet: Arc<Wallet>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let subscriptions = Arc::new(RwLock::new(HashMap::new()));
|
let subscriptions = Arc::new(RwLock::new(HashMap::new()));
|
||||||
let (new_subscription_notif, new_subscription_recv) = mpsc::channel(100);
|
let (new_subscription_notif, new_subscription_recv) = mpsc::channel(100);
|
||||||
@@ -195,6 +206,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop_recv,
|
on_drop_recv,
|
||||||
|
wallet,
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,6 +219,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
||||||
new_subscription_recv: mpsc::Receiver<SubId>,
|
new_subscription_recv: mpsc::Receiver<SubId>,
|
||||||
on_drop_recv: mpsc::Receiver<SubId>,
|
on_drop_recv: mpsc::Receiver<SubId>,
|
||||||
|
wallet: Arc<Wallet>,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "http_subscription",
|
feature = "http_subscription",
|
||||||
@@ -218,6 +231,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop_recv,
|
on_drop_recv,
|
||||||
|
wallet,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
@@ -232,6 +246,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop_recv,
|
on_drop_recv,
|
||||||
|
wallet,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Self::http_worker(
|
Self::http_worker(
|
||||||
@@ -239,6 +254,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop_recv,
|
on_drop_recv,
|
||||||
|
wallet,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,6 +284,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
||||||
new_subscription_recv: mpsc::Receiver<SubId>,
|
new_subscription_recv: mpsc::Receiver<SubId>,
|
||||||
on_drop: mpsc::Receiver<SubId>,
|
on_drop: mpsc::Receiver<SubId>,
|
||||||
|
wallet: Arc<Wallet>,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
let http_worker = http::http_main(
|
let http_worker = http::http_main(
|
||||||
vec![],
|
vec![],
|
||||||
@@ -275,6 +292,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop,
|
on_drop,
|
||||||
|
wallet,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@@ -301,6 +319,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
||||||
new_subscription_recv: mpsc::Receiver<SubId>,
|
new_subscription_recv: mpsc::Receiver<SubId>,
|
||||||
on_drop: mpsc::Receiver<SubId>,
|
on_drop: mpsc::Receiver<SubId>,
|
||||||
|
wallet: Arc<Wallet>,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
tokio::spawn(ws::ws_main(
|
tokio::spawn(ws::ws_main(
|
||||||
http_client,
|
http_client,
|
||||||
@@ -308,6 +327,7 @@ impl SubscriptionClient {
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop,
|
on_drop,
|
||||||
|
wallet,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ use super::http::http_main;
|
|||||||
use super::WsSubscriptionBody;
|
use super::WsSubscriptionBody;
|
||||||
use crate::mint_url::MintUrl;
|
use crate::mint_url::MintUrl;
|
||||||
use crate::pub_sub::SubId;
|
use crate::pub_sub::SubId;
|
||||||
use crate::wallet::client::MintConnector;
|
use crate::wallet::MintConnector;
|
||||||
|
use crate::Wallet;
|
||||||
|
|
||||||
const MAX_ATTEMPT_FALLBACK_HTTP: usize = 10;
|
const MAX_ATTEMPT_FALLBACK_HTTP: usize = 10;
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ async fn fallback_to_http<S: IntoIterator<Item = SubId>>(
|
|||||||
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
||||||
new_subscription_recv: mpsc::Receiver<SubId>,
|
new_subscription_recv: mpsc::Receiver<SubId>,
|
||||||
on_drop: mpsc::Receiver<SubId>,
|
on_drop: mpsc::Receiver<SubId>,
|
||||||
|
wallet: Arc<Wallet>,
|
||||||
) {
|
) {
|
||||||
http_main(
|
http_main(
|
||||||
initial_state,
|
initial_state,
|
||||||
@@ -30,6 +32,7 @@ async fn fallback_to_http<S: IntoIterator<Item = SubId>>(
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop,
|
on_drop,
|
||||||
|
wallet,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -41,6 +44,7 @@ pub async fn ws_main(
|
|||||||
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
subscriptions: Arc<RwLock<HashMap<SubId, WsSubscriptionBody>>>,
|
||||||
mut new_subscription_recv: mpsc::Receiver<SubId>,
|
mut new_subscription_recv: mpsc::Receiver<SubId>,
|
||||||
mut on_drop: mpsc::Receiver<SubId>,
|
mut on_drop: mpsc::Receiver<SubId>,
|
||||||
|
wallet: Arc<Wallet>,
|
||||||
) {
|
) {
|
||||||
let url = mint_url
|
let url = mint_url
|
||||||
.join_paths(&["v1", "ws"])
|
.join_paths(&["v1", "ws"])
|
||||||
@@ -76,6 +80,7 @@ pub async fn ws_main(
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop,
|
on_drop,
|
||||||
|
wallet,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -175,6 +180,7 @@ pub async fn ws_main(
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
new_subscription_recv,
|
new_subscription_recv,
|
||||||
on_drop,
|
on_drop,
|
||||||
|
wallet
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,9 @@
|
|||||||
${_shellHook}
|
${_shellHook}
|
||||||
cargo update
|
cargo update
|
||||||
cargo update -p async-compression --precise 0.4.3
|
cargo update -p async-compression --precise 0.4.3
|
||||||
|
cargo update -p zstd-sys --precise 2.0.8+zstd.1.5.5
|
||||||
|
cargo update -p flate2 --precise 1.0.35
|
||||||
|
|
||||||
|
|
||||||
cargo update -p home --precise 0.5.5
|
cargo update -p home --precise 0.5.5
|
||||||
cargo update -p zerofrom --precise 0.1.5
|
cargo update -p zerofrom --precise 0.1.5
|
||||||
|
|||||||
14
justfile
14
justfile
@@ -35,7 +35,7 @@ format:
|
|||||||
cargo fmt --all
|
cargo fmt --all
|
||||||
nixpkgs-fmt $(echo **.nix)
|
nixpkgs-fmt $(echo **.nix)
|
||||||
|
|
||||||
# run tests
|
# run doc tests
|
||||||
test: build
|
test: build
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -48,6 +48,13 @@ test: build
|
|||||||
cargo test -p cdk-integration-tests --test integration_tests_pure
|
cargo test -p cdk-integration-tests --test integration_tests_pure
|
||||||
cargo test -p cdk-integration-tests --test mint
|
cargo test -p cdk-integration-tests --test mint
|
||||||
|
|
||||||
|
test-all db:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
just test
|
||||||
|
./misc/itests.sh "{{db}}"
|
||||||
|
./misc/fake_itests.sh "{{db}}"
|
||||||
|
|
||||||
|
|
||||||
# run `cargo clippy` on everything
|
# run `cargo clippy` on everything
|
||||||
clippy *ARGS="--locked --offline --workspace --all-targets":
|
clippy *ARGS="--locked --offline --workspace --all-targets":
|
||||||
cargo clippy {{ARGS}}
|
cargo clippy {{ARGS}}
|
||||||
@@ -78,6 +85,11 @@ itest-payment-processor ln:
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
./misc/mintd_payment_processor.sh "{{ln}}"
|
./misc/mintd_payment_processor.sh "{{ln}}"
|
||||||
|
|
||||||
|
|
||||||
|
fake-auth-mint-itest db openid_discovery:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
./misc/fake_auth_itests.sh "{{db}}" "{{openid_discovery}}"
|
||||||
|
|
||||||
run-examples:
|
run-examples:
|
||||||
cargo r --example p2pk
|
cargo r --example p2pk
|
||||||
cargo r --example mint-token
|
cargo r --example mint-token
|
||||||
|
|||||||
109
misc/fake_auth_itests.sh
Executable file
109
misc/fake_auth_itests.sh
Executable file
@@ -0,0 +1,109 @@
|
|||||||
|
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Function to perform cleanup
|
||||||
|
cleanup() {
|
||||||
|
echo "Cleaning up..."
|
||||||
|
|
||||||
|
echo "Killing the cdk mintd"
|
||||||
|
kill -2 $cdk_mintd_pid
|
||||||
|
wait $cdk_mintd_pid
|
||||||
|
|
||||||
|
echo "Mint binary terminated"
|
||||||
|
|
||||||
|
# Remove the temporary directory
|
||||||
|
rm -rf "$cdk_itests"
|
||||||
|
echo "Temp directory removed: $cdk_itests"
|
||||||
|
unset cdk_itests
|
||||||
|
unset cdk_itests_mint_addr
|
||||||
|
unset cdk_itests_mint_port
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set up trap to call cleanup on script exit
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Create a temporary directory
|
||||||
|
export cdk_itests=$(mktemp -d)
|
||||||
|
export cdk_itests_mint_addr="127.0.0.1";
|
||||||
|
export cdk_itests_mint_port=8087;
|
||||||
|
|
||||||
|
# Check if the temporary directory was created successfully
|
||||||
|
if [[ ! -d "$cdk_itests" ]]; then
|
||||||
|
echo "Failed to create temp directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Temp directory created: $cdk_itests"
|
||||||
|
export MINT_DATABASE="$1";
|
||||||
|
export OPENID_DISCOVERY="$2";
|
||||||
|
|
||||||
|
cargo build -p cdk-integration-tests
|
||||||
|
|
||||||
|
export CDK_MINTD_URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port";
|
||||||
|
export CDK_MINTD_WORK_DIR="$cdk_itests";
|
||||||
|
export CDK_MINTD_LISTEN_HOST=$cdk_itests_mint_addr;
|
||||||
|
export CDK_MINTD_LISTEN_PORT=$cdk_itests_mint_port;
|
||||||
|
export CDK_MINTD_LN_BACKEND="fakewallet";
|
||||||
|
export CDK_MINTD_FAKE_WALLET_SUPPORTED_UNITS="sat";
|
||||||
|
export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal";
|
||||||
|
export CDK_MINTD_FAKE_WALLET_FEE_PERCENT="0";
|
||||||
|
export CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN="1";
|
||||||
|
export CDK_MINTD_DATABASE=$MINT_DATABASE;
|
||||||
|
|
||||||
|
# Auth configuration
|
||||||
|
export CDK_TEST_OIDC_USER="cdk-test";
|
||||||
|
export CDK_TEST_OIDC_PASSWORD="cdkpassword";
|
||||||
|
|
||||||
|
export CDK_MINTD_AUTH_OPENID_DISCOVERY=$OPENID_DISCOVERY;
|
||||||
|
export CDK_MINTD_AUTH_OPENID_CLIENT_ID="cashu-client";
|
||||||
|
export CDK_MINTD_AUTH_MINT_MAX_BAT="50";
|
||||||
|
export CDK_MINTD_AUTH_ENABLED_MINT="true";
|
||||||
|
export CDK_MINTD_AUTH_ENABLED_MELT="true";
|
||||||
|
export CDK_MINTD_AUTH_ENABLED_SWAP="true";
|
||||||
|
export CDK_MINTD_AUTH_ENABLED_CHECK_MINT_QUOTE="true";
|
||||||
|
export CDK_MINTD_AUTH_ENABLED_CHECK_MELT_QUOTE="true";
|
||||||
|
export CDK_MINTD_AUTH_ENABLED_RESTORE="true";
|
||||||
|
export CDK_MINTD_AUTH_ENABLED_CHECK_PROOF_STATE="true";
|
||||||
|
|
||||||
|
echo "Starting auth mintd";
|
||||||
|
cargo run --bin cdk-mintd --features redb &
|
||||||
|
cdk_mintd_pid=$!
|
||||||
|
|
||||||
|
URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port/v1/info"
|
||||||
|
TIMEOUT=100
|
||||||
|
START_TIME=$(date +%s)
|
||||||
|
# Loop until the endpoint returns a 200 OK status or timeout is reached
|
||||||
|
while true; do
|
||||||
|
# Get the current time
|
||||||
|
CURRENT_TIME=$(date +%s)
|
||||||
|
|
||||||
|
# Calculate the elapsed time
|
||||||
|
ELAPSED_TIME=$((CURRENT_TIME - START_TIME))
|
||||||
|
|
||||||
|
# Check if the elapsed time exceeds the timeout
|
||||||
|
if [ $ELAPSED_TIME -ge $TIMEOUT ]; then
|
||||||
|
echo "Timeout of $TIMEOUT seconds reached. Exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make a request to the endpoint and capture the HTTP status code
|
||||||
|
HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" $URL)
|
||||||
|
|
||||||
|
# Check if the HTTP status is 200 OK
|
||||||
|
if [ "$HTTP_STATUS" -eq 200 ]; then
|
||||||
|
echo "Received 200 OK from $URL"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "Waiting for 200 OK response, current status: $HTTP_STATUS"
|
||||||
|
sleep 2 # Wait for 2 seconds before retrying
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run cargo test
|
||||||
|
cargo test -p cdk-integration-tests --test fake_auth
|
||||||
|
|
||||||
|
# Capture the exit status of cargo test
|
||||||
|
test_status=$?
|
||||||
|
|
||||||
|
# Exit with the status of the test
|
||||||
|
exit $test_status
|
||||||
7
misc/keycloak/.env.example
Normal file
7
misc/keycloak/.env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
POSTGRES_DB=keycloak_db
|
||||||
|
POSTGRES_USER=keycloak_db_user
|
||||||
|
POSTGRES_PASSWORD=keycloak_db_user_password
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=password
|
||||||
|
KC_HOSTNAME=localhost
|
||||||
|
KC_HOSTNAME_PORT=8080
|
||||||
45
misc/keycloak/docker-compose-recover.yml
Normal file
45
misc/keycloak/docker-compose-recover.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16.4
|
||||||
|
volumes:
|
||||||
|
- ./postgres_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: cashu
|
||||||
|
POSTGRES_USER: cashu
|
||||||
|
POSTGRES_PASSWORD: cashu
|
||||||
|
networks:
|
||||||
|
- keycloak_network
|
||||||
|
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:25.0.6
|
||||||
|
command: start --import-realm
|
||||||
|
volumes:
|
||||||
|
- ./keycloak-export:/opt/keycloak/data/import
|
||||||
|
environment:
|
||||||
|
KC_HOSTNAME: localhost
|
||||||
|
KC_HOSTNAME_PORT: 8080
|
||||||
|
KC_HOSTNAME_STRICT_BACKCHANNEL: false
|
||||||
|
KC_HTTP_ENABLED: true
|
||||||
|
KC_HOSTNAME_STRICT_HTTPS: false
|
||||||
|
KC_HEALTH_ENABLED: true
|
||||||
|
KEYCLOAK_ADMIN: admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
|
KC_DB: postgres
|
||||||
|
KC_DB_URL: jdbc:postgresql://postgres/cashu
|
||||||
|
KC_DB_USERNAME: cashu
|
||||||
|
KC_DB_PASSWORD: cashu
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- keycloak_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
keycloak_network:
|
||||||
|
driver: bridge
|
||||||
43
misc/keycloak/docker-compose.yml
Normal file
43
misc/keycloak/docker-compose.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16.4
|
||||||
|
volumes:
|
||||||
|
- ./postgres_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
networks:
|
||||||
|
- keycloak_network
|
||||||
|
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:25.0.6
|
||||||
|
command: start
|
||||||
|
environment:
|
||||||
|
KC_HOSTNAME: localhost
|
||||||
|
KC_HOSTNAME_PORT: 8080
|
||||||
|
KC_HOSTNAME_STRICT_BACKCHANNEL: false
|
||||||
|
KC_HTTP_ENABLED: true
|
||||||
|
KC_HOSTNAME_STRICT_HTTPS: false
|
||||||
|
KC_HEALTH_ENABLED: true
|
||||||
|
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN}
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
|
||||||
|
KC_DB: postgres
|
||||||
|
KC_DB_URL: jdbc:postgresql://postgres/${POSTGRES_DB}
|
||||||
|
KC_DB_USERNAME: ${POSTGRES_USER}
|
||||||
|
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- keycloak_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
keycloak_network:
|
||||||
|
driver: bridge
|
||||||
1854
misc/keycloak/keycloak-export/cdk-test-realm-realm.json
Normal file
1854
misc/keycloak/keycloak-export/cdk-test-realm-realm.json
Normal file
File diff suppressed because it is too large
Load Diff
27
misc/keycloak/keycloak-export/cdk-test-realm-users-0.json
Normal file
27
misc/keycloak/keycloak-export/cdk-test-realm-users-0.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"realm" : "cdk-test-realm",
|
||||||
|
"users" : [ {
|
||||||
|
"id" : "6ea5cccc-bb0d-4757-a676-2515056fb4c6",
|
||||||
|
"username" : "cdk-test",
|
||||||
|
"firstName" : "test",
|
||||||
|
"lastName" : "test",
|
||||||
|
"email" : "test@email.com",
|
||||||
|
"emailVerified" : false,
|
||||||
|
"createdTimestamp" : 1740501176107,
|
||||||
|
"enabled" : true,
|
||||||
|
"totp" : false,
|
||||||
|
"credentials" : [ {
|
||||||
|
"id" : "a184d280-ee03-48c1-bd99-b60cbec7f828",
|
||||||
|
"type" : "password",
|
||||||
|
"userLabel" : "My password",
|
||||||
|
"createdDate" : 1740501197395,
|
||||||
|
"secretData" : "{\"value\":\"qIQdnJ76MfwoPE0NgGxjpNPOrWvlvnIhreaCA0fX88g=\",\"salt\":\"npddG2eT8Ofp/M2QORJu0Q==\",\"additionalParameters\":{}}",
|
||||||
|
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
||||||
|
} ],
|
||||||
|
"disableableCredentialTypes" : [ ],
|
||||||
|
"requiredActions" : [ ],
|
||||||
|
"realmRoles" : [ "default-roles-cdk-test-realm" ],
|
||||||
|
"notBefore" : 0,
|
||||||
|
"groups" : [ ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
2050
misc/keycloak/keycloak-export/master-realm.json
Normal file
2050
misc/keycloak/keycloak-export/master-realm.json
Normal file
File diff suppressed because it is too large
Load Diff
26
misc/keycloak/keycloak-export/master-users-0.json
Normal file
26
misc/keycloak/keycloak-export/master-users-0.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"realm" : "master",
|
||||||
|
"users" : [ {
|
||||||
|
"id" : "3ba20234-cfa8-4ff7-ae64-dac4870470e5",
|
||||||
|
"username" : "admin",
|
||||||
|
"emailVerified" : false,
|
||||||
|
"createdTimestamp" : 1740500953408,
|
||||||
|
"enabled" : true,
|
||||||
|
"totp" : false,
|
||||||
|
"credentials" : [ {
|
||||||
|
"id" : "98ceebfb-8dbe-4842-a335-e900d84eb2ef",
|
||||||
|
"type" : "password",
|
||||||
|
"createdDate" : 1740500953499,
|
||||||
|
"secretData" : "{\"value\":\"J5GuTpzJwSHGrfMzp7yPoBBmyQZ+Ijk5AozNbxkHcI0=\",\"salt\":\"uVtNH1+Non3hg/mKYy3XMQ==\",\"additionalParameters\":{}}",
|
||||||
|
"credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
|
||||||
|
} ],
|
||||||
|
"disableableCredentialTypes" : [ ],
|
||||||
|
"requiredActions" : [ ],
|
||||||
|
"realmRoles" : [ "default-roles-master", "admin" ],
|
||||||
|
"clientRoles" : {
|
||||||
|
"cdk-test-realm-realm" : [ "view-identity-providers", "manage-realm", "query-users", "manage-users", "create-client", "view-events", "manage-clients", "view-clients", "view-authorization", "manage-authorization", "view-realm", "manage-identity-providers", "query-realms", "manage-events", "view-users", "query-clients", "query-groups" ]
|
||||||
|
},
|
||||||
|
"notBefore" : 0,
|
||||||
|
"groups" : [ ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user