Skip to content
Open
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
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,7 @@ jobs:
matrix:
ln:
[
FAKEWALLET,
CLN,
LND
FAKEWALLET
]
steps:
- name: checkout
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/daily-flake-check.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Daily Flake Check

on:
schedule:
# Run daily at 6 AM UTC
- cron: '0 6 * * *'
# schedule:
# # Run daily at 6 AM UTC
# - cron: '0 6 * * *'
# Allow manual trigger
workflow_dispatch:

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/docker-publish-arm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Publish Docker Image ARM64
on:
push:
branches: [main]
pull_request:
branches: [main]
release:
types: [published]
workflow_dispatch:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Publish Docker Image AMD64
on:
push:
branches: [main]
pull_request:
branches: [main]
release:
types: [published]
workflow_dispatch:
Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ bare_urls = "warn"

[workspace.package]
edition = "2021"
rust-version = "1.85.0"
rust-version = "1.88.0"
license = "MIT"
homepage = "https://github.com/cashubtc/cdk"
repository = "https://github.com/cashubtc/cdk.git"
Expand Down Expand Up @@ -114,6 +114,8 @@ strum_macros = "0.27.1"
rustls = { version = "0.23.27", default-features = false, features = ["ring"] }
prometheus = { version = "0.13.4", features = ["process"], default-features = false }

cdk-square = { path = "./crates/cdk-square", version = "=0.13.0" }




Expand Down
8 changes: 8 additions & 0 deletions crates/cdk-common/src/database/mint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,14 @@ pub trait KVStoreTransaction<'a, Error>: DbTransactionFinalizer<Err = Error> {
primary_namespace: &str,
secondary_namespace: &str,
) -> Result<Vec<String>, Error>;

/// Remove values from key-value store where the value was updated before a given time
async fn kv_remove_older_than(
&mut self,
primary_namespace: &str,
secondary_namespace: &str,
expiry_time: u64,
) -> Result<(), Error>;
}

/// Base database writer
Expand Down
29 changes: 29 additions & 0 deletions crates/cdk-common/src/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,19 @@ pub trait MintPayment {
Ok(())
}

/// Check if a payment is internal to this payment processor
/// Used for internal settlement validation
/// Returns:
/// - `Ok(Some(true))` if payment is internal
/// - `Ok(Some(false))` if payment is definitely not internal
/// - `Ok(None)` if processor doesn't implement this check
async fn is_internal_payment(
&self,
_request: &Bolt11Invoice,
) -> Result<Option<bool>, Self::Err> {
Ok(None)
}

/// Base Settings
async fn get_settings(&self) -> Result<serde_json::Value, Self::Err>;

Expand Down Expand Up @@ -611,6 +624,22 @@ where

result
}

async fn is_internal_payment(
&self,
request: &Bolt11Invoice,
) -> Result<Option<bool>, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("is_internal_payment");

let result = self.inner.is_internal_payment(request).await;

let duration = start.elapsed().as_secs_f64();
METRICS.record_mint_operation_histogram("is_internal_payment", result.is_ok(), duration);
METRICS.dec_in_flight_requests("is_internal_payment");

result
}
}

/// Type alias for Mint Payment trait
Expand Down
6 changes: 6 additions & 0 deletions crates/cdk-fake-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ rust-version.workspace = true # MSRV
description = "CDK fake ln backend"
readme = "README.md"

[features]
default = ["square"]
square = ["dep:cdk-square"]

[dependencies]
async-trait.workspace = true
bitcoin.workspace = true
Expand All @@ -26,3 +30,5 @@ lightning.workspace = true
tokio-stream.workspace = true
reqwest.workspace = true
uuid.workspace = true
axum = { workspace = true }
cdk-square = { workspace = true, optional = true }
4 changes: 4 additions & 0 deletions crates/cdk-fake-wallet/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ pub enum Error {
/// Unknown invoice
#[error("No channel receiver")]
NoReceiver,
/// Square error
#[cfg(feature = "square")]
#[error(transparent)]
Square(#[from] cdk_square::error::Error),
}

impl From<Error> for cdk_common::payment::Error {
Expand Down
108 changes: 108 additions & 0 deletions crates/cdk-fake-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ use std::sync::Arc;
use std::time::{Duration, Instant};

use async_trait::async_trait;
#[cfg(feature = "square")]
use axum::Router;
use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::{Secp256k1, SecretKey};
use cdk_common::amount::{to_unit, Amount};
use cdk_common::common::FeeReserve;
#[cfg(feature = "square")]
use cdk_common::database::mint::DynMintKVStore;
use cdk_common::ensure_cdk;
use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
use cdk_common::payment::{
Expand All @@ -48,6 +52,10 @@ use uuid::Uuid;

pub mod error;

// Re-export Square types for convenience
#[cfg(feature = "square")]
pub use cdk_square::{Square, SquareConfig};

/// Default maximum size for the secondary repayment queue
const DEFAULT_REPAY_QUEUE_MAX_SIZE: usize = 100;

Expand Down Expand Up @@ -336,6 +344,9 @@ pub struct FakeWallet {
unit: CurrencyUnit,
secondary_repayment_queue: SecondaryRepaymentQueue,
exchange_rate_cache: ExchangeRateCache,
/// Optional Square payment backend
#[cfg(feature = "square")]
square: Option<Arc<cdk_square::Square>>,
}

impl FakeWallet {
Expand All @@ -357,6 +368,60 @@ impl FakeWallet {
)
}

/// Create new [`FakeWallet`] with Square integration
#[cfg(feature = "square")]
#[allow(clippy::too_many_arguments)]
pub async fn new_with_square(
fee_reserve: FeeReserve,
payment_states: HashMap<String, MeltQuoteState>,
fail_payment_check: HashSet<String>,
payment_delay: u64,
unit: CurrencyUnit,
kv_store: DynMintKVStore,
square_config: Option<cdk_square::SquareConfig>,
square_webhook_url: Option<String>,
) -> Result<Self, error::Error> {
let (sender, receiver) = tokio::sync::mpsc::channel(8);
let incoming_payments = Arc::new(RwLock::new(HashMap::new()));

let secondary_repayment_queue = SecondaryRepaymentQueue::new(
DEFAULT_REPAY_QUEUE_MAX_SIZE,
sender.clone(),
unit.clone(),
);

let square = match square_config {
Some(config) => {
match cdk_square::Square::from_config(config, square_webhook_url, kv_store.clone())
{
Ok(square_backend) => {
let square_arc = Arc::new(square_backend);
square_arc.start().await?;
Some(square_arc)
}
Err(e) => return Err(e.into()),
}
}
None => None,
};

Ok(Self {
fee_reserve,
sender,
receiver: Arc::new(Mutex::new(Some(receiver))),
payment_states: Arc::new(Mutex::new(payment_states)),
failed_payment_check: Arc::new(Mutex::new(fail_payment_check)),
payment_delay,
wait_invoice_cancel_token: CancellationToken::new(),
wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
incoming_payments,
unit,
secondary_repayment_queue,
exchange_rate_cache: ExchangeRateCache::new(),
square,
})
}

/// Create new [`FakeWallet`] with custom secondary repayment queue size
pub fn new_with_repay_queue_size(
fee_reserve: FeeReserve,
Expand Down Expand Up @@ -385,8 +450,22 @@ impl FakeWallet {
unit,
secondary_repayment_queue,
exchange_rate_cache: ExchangeRateCache::new(),
#[cfg(feature = "square")]
square: None,
}
}

/// Create Square webhook router for payment notifications
///
/// # Returns
/// * `Some(Router)` if Square backend is configured with a webhook URL
/// * `None` if Square backend is not configured or is using polling mode
#[cfg(feature = "square")]
pub fn create_square_webhook_router(&self) -> Option<Router> {
self.square
.as_ref()
.and_then(|square| square.create_webhook_router())
}
}

/// Struct for signaling what methods should respond via invoice description
Expand Down Expand Up @@ -428,6 +507,35 @@ impl MintPayment for FakeWallet {
})?)
}

#[instrument(skip_all)]
async fn is_internal_payment(
&self,
request: &Bolt11Invoice,
) -> Result<Option<bool>, Self::Err> {
#[cfg(feature = "square")]
{
let Some(square) = self.square.as_ref() else {
return Ok(None);
};

// Check if this invoice exists in our Square tracking
square
.check_invoice_exists(request)
.await
.map_err(|e| {
tracing::error!("Error checking Square invoice: {}", e);
cdk_common::payment::Error::Custom(format!("Square error: {}", e))
})
.map(Some)
}

#[cfg(not(feature = "square"))]
{
let _ = request; // Avoid unused variable warning
Ok(None)
}
}

#[instrument(skip_all)]
fn is_wait_invoice_active(&self) -> bool {
self.wait_invoice_is_active.load(Ordering::SeqCst)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ async fn start_fake_auth_mint(
reserve_fee_min: cdk::Amount::from(1),
min_delay_time: 1,
max_delay_time: 3,
square: None,
};

let mut settings = shared::create_fake_wallet_settings(
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-integration-tests/src/bin/start_fake_mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async fn start_fake_mint(
reserve_fee_min: 1.into(),
min_delay_time: 1,
max_delay_time: 3,
square: None,
});

// Create settings struct for fake mint using shared function
Expand Down
6 changes: 5 additions & 1 deletion crates/cdk-mintd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ rust-version.workspace = true
readme = "README.md"

[features]
default = ["management-rpc", "cln", "lnd", "lnbits", "fakewallet", "grpc-processor", "sqlite", "strike", "nwc"]
default = ["management-rpc", "cln", "lnd", "lnbits", "fakewallet", "grpc-processor", "sqlite", "strike", "nwc", "square"]
# Database features - at least one must be enabled
sqlite = ["dep:cdk-sqlite"]
postgres = ["dep:cdk-postgres"]
Expand All @@ -31,6 +31,8 @@ auth = ["cdk/auth", "cdk-axum/auth", "cdk-sqlite?/auth", "cdk-postgres?/auth"]
prometheus = ["cdk/prometheus", "dep:cdk-prometheus", "cdk-sqlite?/prometheus", "cdk-axum/prometheus"]
strike = ["dep:cdk-strike"]

square = ["dep:cdk-square", "cdk-fake-wallet?/square", "cdk-nwc?/square"]

[dependencies]
anyhow.workspace = true
async-trait.workspace = true
Expand Down Expand Up @@ -72,4 +74,6 @@ home.workspace = true
utoipa = { workspace = true, optional = true }
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true }

cdk-square = { workspace = true, optional = true }

[build-dependencies]
14 changes: 14 additions & 0 deletions crates/cdk-mintd/example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,24 @@ reserve_fee_min = 1
min_delay_time = 1
max_delay_time = 3

# Optional Square integration for fake wallet (for testing Square merchants)
# [fake_wallet.square]
# api_token = "your_square_api_token_here"
# environment = "SANDBOX" # or "PRODUCTION"
# webhook_enabled = true # If false, uses polling mode (syncs every 5 seconds) instead of webhooks
# database_url = "postgresql://user:password@host:port/database?sslmode=require" # PostgreSQL connection for OAuth credentials

# [nwc]
# nwc_uri = "nostr+walletconnect://..."
# fee_percent = 0.02
# reserve_fee_min = 1
#
# # Optional Square integration for NWC backend
# [nwc.square]
# api_token = "your_square_api_token_here"
# environment = "SANDBOX" # or "PRODUCTION"
# webhook_enabled = true # If false, uses polling mode (syncs every 5 seconds) instead of webhooks
# database_url = "postgresql://user:password@host:port/database?sslmode=require" # PostgreSQL connection for OAuth credentials

# [grpc_processor]
# gRPC Payment Processor configuration
Expand Down
Loading
Loading