Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,526 changes: 642 additions & 884 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ nostr-sdk = { version = "0.44.1", default-features = false, features = [
"nip59"
]}
bitcoin-payment-instructions = { version = "0.7.0", default-features = false }
dlc-messages = { version = "0.8.0" }



Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ WORKDIR /usr/src/app
COPY flake.nix ./flake.nix
COPY Cargo.toml ./Cargo.toml
COPY crates ./crates
COPY bindings ./bindings

# Start the Nix daemon and develop the environment
RUN nix develop --extra-experimental-features nix-command --extra-experimental-features flakes --command cargo build --release --bin cdk-mintd --features postgres --features prometheus
RUN nix develop --extra-experimental-features nix-command --extra-experimental-features flakes --command cargo build --release --bin cdk-mintd --features postgres --features prometheus --features conditional-tokens

# Create a runtime stage
FROM debian:trixie-slim
Expand All @@ -20,7 +21,7 @@ WORKDIR /usr/src/app

# Install needed runtime dependencies (if any)
RUN apt-get update && \
apt-get install -y --no-install-recommends patchelf && \
apt-get install -y --no-install-recommends patchelf curl && \
rm -rf /var/lib/apt/lists/*

# Copy the built application from the build stage
Expand Down
3 changes: 3 additions & 0 deletions crates/cashu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ default = ["mint", "wallet"]
swagger = ["dep:utoipa"]
mint = []
wallet = []
conditional-tokens = ["dep:dlc-messages"]
test-utils = ["conditional-tokens"]
nostr = ["dep:nostr-sdk"]
bench = []

Expand All @@ -36,6 +38,7 @@ serde_with.workspace = true
strum.workspace = true
strum_macros.workspace = true
nostr-sdk = { workspace = true, optional = true }
dlc-messages = { workspace = true, optional = true }
zeroize = "1"
web-time.workspace = true
unicode-normalization = "0.1.25"
Expand Down
8 changes: 8 additions & 0 deletions crates/cashu/examples/nut-ctf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

use cashu::nuts::nut_ctf::NutCtfSettings;

fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Hello, world!");

Ok(())
}
72 changes: 62 additions & 10 deletions crates/cashu/src/nuts/auth/nut21.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,21 @@ pub enum RoutePath {
MintBlindAuth,
/// WebSocket
Ws,
/// Conditions (GET/POST /v1/conditions)
#[cfg(feature = "conditional-tokens")]
Conditions,
/// Single Condition (GET /v1/conditions/{id})
#[cfg(feature = "conditional-tokens")]
Condition,
/// Condition Partitions (POST /v1/conditions/{id}/partitions)
#[cfg(feature = "conditional-tokens")]
ConditionPartitions,
/// Conditional Keysets (GET /v1/conditional_keysets)
#[cfg(feature = "conditional-tokens")]
ConditionalKeysets,
/// Redeem Outcome (POST /v1/redeem_outcome)
#[cfg(feature = "conditional-tokens")]
RedeemOutcome,
}

impl Serialize for RoutePath {
Expand All @@ -161,21 +176,36 @@ impl std::str::FromStr for RoutePath {
"/v1/restore" => Ok(RoutePath::Restore),
"/v1/auth/blind/mint" => Ok(RoutePath::MintBlindAuth),
"/v1/ws" => Ok(RoutePath::Ws),
#[cfg(feature = "conditional-tokens")]
"/v1/conditions" => Ok(RoutePath::Conditions),
#[cfg(feature = "conditional-tokens")]
"/v1/conditional_keysets" => Ok(RoutePath::ConditionalKeysets),
#[cfg(feature = "conditional-tokens")]
"/v1/redeem_outcome" => Ok(RoutePath::RedeemOutcome),
_ => {
// Try to parse as a payment method route
if let Some(method) = s.strip_prefix("/v1/mint/quote/") {
Ok(RoutePath::MintQuote(method.to_string()))
return Ok(RoutePath::MintQuote(method.to_string()));
} else if let Some(method) = s.strip_prefix("/v1/mint/") {
Ok(RoutePath::Mint(method.to_string()))
return Ok(RoutePath::Mint(method.to_string()));
} else if let Some(method) = s.strip_prefix("/v1/melt/quote/") {
Ok(RoutePath::MeltQuote(method.to_string()))
return Ok(RoutePath::MeltQuote(method.to_string()));
} else if let Some(method) = s.strip_prefix("/v1/melt/") {
Ok(RoutePath::Melt(method.to_string()))
} else {
// Unknown path - this might be an old database value or config
// Provide a helpful error message
Err(Error::UnknownRoute(s.to_string()))
return Ok(RoutePath::Melt(method.to_string()));
}
// Conditional token paths with dynamic segments
#[cfg(feature = "conditional-tokens")]
{
if let Some(rest) = s.strip_prefix("/v1/conditions/") {
if rest.ends_with("/partitions") {
return Ok(RoutePath::ConditionPartitions);
}
return Ok(RoutePath::Condition);
}
}
// Unknown path - this might be an old database value or config
// Provide a helpful error message
Err(Error::UnknownRoute(s.to_string()))
}
}
}
Expand All @@ -195,13 +225,23 @@ impl RoutePath {
/// Get all non-payment-method route paths
/// These are routes that don't depend on payment methods
pub fn static_paths() -> Vec<RoutePath> {
vec![
#[allow(unused_mut)]
let mut paths = vec![
RoutePath::Swap,
RoutePath::Checkstate,
RoutePath::Restore,
RoutePath::MintBlindAuth,
RoutePath::Ws,
]
];
#[cfg(feature = "conditional-tokens")]
{
paths.push(RoutePath::Conditions);
paths.push(RoutePath::Condition);
paths.push(RoutePath::ConditionPartitions);
paths.push(RoutePath::ConditionalKeysets);
paths.push(RoutePath::RedeemOutcome);
}
paths
}

/// Get all route paths for common payment methods (bolt11, bolt12)
Expand Down Expand Up @@ -271,6 +311,18 @@ impl std::fmt::Display for RoutePath {
RoutePath::Restore => write!(f, "/v1/restore"),
RoutePath::MintBlindAuth => write!(f, "/v1/auth/blind/mint"),
RoutePath::Ws => write!(f, "/v1/ws"),
#[cfg(feature = "conditional-tokens")]
RoutePath::Conditions => write!(f, "/v1/conditions"),
#[cfg(feature = "conditional-tokens")]
RoutePath::Condition => write!(f, "/v1/conditions/{{condition_id}}"),
#[cfg(feature = "conditional-tokens")]
RoutePath::ConditionPartitions => {
write!(f, "/v1/conditions/{{condition_id}}/partitions")
}
#[cfg(feature = "conditional-tokens")]
RoutePath::ConditionalKeysets => write!(f, "/v1/conditional_keysets"),
#[cfg(feature = "conditional-tokens")]
RoutePath::RedeemOutcome => write!(f, "/v1/redeem_outcome"),
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/cashu/src/nuts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub mod nut27;
#[cfg(feature = "wallet")]
pub mod nut28;
pub mod nut29;
#[cfg(feature = "conditional-tokens")]
pub mod nut_ctf;

mod auth;

Expand Down
30 changes: 30 additions & 0 deletions crates/cashu/src/nuts/nut00/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ use crate::nuts::nut01::SecretKey;
use crate::nuts::nut11::{serde_p2pk_witness, P2PKWitness};
use crate::nuts::nut12::BlindSignatureDleq;
use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness};
#[cfg(feature = "conditional-tokens")]
use crate::nuts::nut_ctf::{serde_oracle_witness, OracleWitness};
use crate::nuts::{Id, ProofDleq};
use crate::secret::Secret;
use crate::Amount;
Expand Down Expand Up @@ -315,6 +317,10 @@ pub enum Witness {
/// P2PK Witness
#[serde(with = "serde_p2pk_witness")]
P2PKWitness(P2PKWitness),
/// Oracle Witness (NUT-CTF conditional tokens)
#[cfg(feature = "conditional-tokens")]
#[serde(with = "serde_oracle_witness")]
OracleWitness(OracleWitness),
}

impl From<P2PKWitness> for Witness {
Expand All @@ -329,6 +335,13 @@ impl From<HTLCWitness> for Witness {
}
}

#[cfg(feature = "conditional-tokens")]
impl From<OracleWitness> for Witness {
fn from(witness: OracleWitness) -> Self {
Self::OracleWitness(witness)
}
}

impl Witness {
/// Add signatures to [`Witness`]
pub fn add_signatures(&mut self, signatures: Vec<String>) {
Expand All @@ -338,6 +351,10 @@ impl Witness {
Some(sigs) => sigs.extend(signatures),
None => htlc_witness.signatures = Some(signatures),
},
#[cfg(feature = "conditional-tokens")]
Self::OracleWitness(_) => {
// Oracle witnesses don't use signature-style signatures
}
}
}

Expand All @@ -346,6 +363,8 @@ impl Witness {
match self {
Self::P2PKWitness(witness) => Some(witness.signatures.clone()),
Self::HTLCWitness(witness) => witness.signatures.clone(),
#[cfg(feature = "conditional-tokens")]
Self::OracleWitness(_) => None,
}
}

Expand All @@ -354,6 +373,17 @@ impl Witness {
match self {
Self::P2PKWitness(_witness) => None,
Self::HTLCWitness(witness) => Some(witness.preimage.clone()),
#[cfg(feature = "conditional-tokens")]
Self::OracleWitness(_) => None,
}
}

/// Get oracle witness from [`Witness`] (NUT-CTF)
#[cfg(feature = "conditional-tokens")]
pub fn oracle_witness(&self) -> Option<&OracleWitness> {
match self {
Self::OracleWitness(w) => Some(w),
_ => None,
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions crates/cashu/src/nuts/nut02.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,58 @@ impl Id {
}
}

/// *** V2 KEYSET (conditional) ***
/// Create [`Id`] v2 from keys, unit, and conditional token parameters.
/// Builds same base preimage as `v2_from_data`, then appends
/// `|condition_id:{hex}|outcome_collection_id:{hex}`
#[cfg(feature = "mint")]
pub fn v2_from_data_conditional(
map: &Keys,
unit: &CurrencyUnit,
input_fee_ppk: u64,
expiry: Option<u64>,
condition_id: &str,
outcome_collection_id: &str,
) -> Self {
let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
keys.sort_by_key(|(amt, _v)| *amt);

let keys_string = keys
.iter()
.map(|(amt, pubkey)| format!("{}:{}", amt, hex::encode(pubkey.to_bytes())))
.collect::<Vec<String>>()
.join(",");

let mut data = keys_string;
data.push_str(&format!("|unit:{}", unit));

if input_fee_ppk > 0 {
data.push_str(&format!("|input_fee_ppk:{}", input_fee_ppk));
}

if let Some(expiry) = expiry {
if expiry > 0 {
data.push_str(&format!("|final_expiry:{}", expiry));
}
}

data.push_str(&format!("|condition_id:{}", condition_id));
data.push_str(&format!("|outcome_collection_id:{}", outcome_collection_id));

let hash = Sha256::hash(data.as_bytes());
let hex_of_hash = hex::encode(hash.to_byte_array());

Self {
version: KeySetVersion::Version01,
id: IdBytes::V2(
hex::decode(&hex_of_hash[0..Self::STRLEN_V2])
.expect("Keys hash could not be hex decoded")
.try_into()
.expect("Invalid length of hex id"),
),
}
}

/// Selects the correct IDv2 from a list of keysets and the given short-id
/// or returns the short-id in the case of v1.
pub fn from_short_keyset_id(
Expand Down
18 changes: 18 additions & 0 deletions crates/cashu/src/nuts/nut06.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,24 @@ pub struct Nuts {
#[serde(default)]
#[serde(rename = "20")]
pub nut20: SupportedSettings,
/// NUT-CTF Settings (Conditional Token Framework)
#[serde(rename = "CTF")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg(feature = "conditional-tokens")]
pub nut_ctf: Option<super::nut_ctf::NutCtfSettings>,
/// NUT-CTF-split-merge Settings (CTF Split/Merge)
#[serde(rename = "CTF-split-merge")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg(feature = "conditional-tokens")]
pub nut_ctf_split_merge: Option<super::nut_ctf::NutCtfSplitMergeSettings>,
/// NUT-CTF-numeric Settings (Numeric Conditional Tokens)
#[serde(rename = "CTF-numeric")]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg(feature = "conditional-tokens")]
pub nut_ctf_numeric: Option<super::nut_ctf::NutCtfNumericSettings>,
/// NUT21 Settings
#[serde(rename = "21")]
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
1 change: 0 additions & 1 deletion crates/cashu/src/nuts/nut17/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use crate::nuts::{
};
use crate::quote_id::QuoteIdError;
use crate::MintQuoteBolt12Response;

pub mod ws;

/// Subscription Parameter according to the standard
Expand Down
Loading