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
55 changes: 48 additions & 7 deletions crates/cdk-agicash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ use square_api::SquareApiClient;
/// Hardcoded lookup window (seconds on each side of invoice timestamp)
pub const SQUARE_PAYMENT_LOOKUP_WINDOW_SECS: u64 = 2;

/// Format epoch seconds as a human-readable UTC string for log messages.
fn epoch_to_human(epoch: u64) -> String {
square_api::epoch_to_rfc3339(epoch)
}

/// Validation strategy for closed-loop payments
#[allow(missing_debug_implementations)]
pub enum ClosedLoopValidator {
Expand All @@ -43,6 +48,8 @@ pub enum ClosedLoopValidator {
api_client: SquareApiClient,
/// Merchant name shown in rejection error messages
valid_destination_name: String,
/// Active Square location IDs to search for payments across all locations
location_ids: Vec<String>,
},
/// Internal validation — only invoices created by this mint are allowed.
///
Expand Down Expand Up @@ -88,12 +95,14 @@ impl ClosedLoopPayment {
inner: Arc<dyn MintPayment<Err = payment::Error> + Send + Sync>,
api_client: SquareApiClient,
valid_destination_name: String,
location_ids: Vec<String>,
) -> Self {
Self {
inner,
validator: ClosedLoopValidator::Square {
api_client,
valid_destination_name,
location_ids,
},
}
}
Expand All @@ -118,7 +127,11 @@ impl ClosedLoopPayment {
ClosedLoopValidator::Square {
api_client,
valid_destination_name,
} => Self::validate_square(bolt11, api_client, valid_destination_name).await,
location_ids,
} => {
Self::validate_square(bolt11, api_client, valid_destination_name, location_ids)
.await
}
ClosedLoopValidator::Internal {
known_hashes,
destination_name,
Expand All @@ -131,6 +144,7 @@ impl ClosedLoopPayment {
bolt11: &Bolt11Invoice,
api_client: &SquareApiClient,
valid_destination_name: &str,
location_ids: &[String],
) -> Result<(), payment::Error> {
// Pre-filter: check description contains valid_destination_name
let (description_matches, is_hashed) = match bolt11.description() {
Expand Down Expand Up @@ -166,25 +180,52 @@ impl ClosedLoopPayment {

// Check invoice timestamp against Square payment timestamps
let invoice_epoch = bolt11.duration_since_epoch().as_secs();
let invoice_time = epoch_to_human(invoice_epoch);

tracing::debug!(
invoice_epoch,
invoice_time = %invoice_time,
window_secs = SQUARE_PAYMENT_LOOKUP_WINDOW_SECS,
destination = %valid_destination_name,
"Checking invoice against Square payments"
);

if api_client
.has_lightning_payment_near_timestamp(invoice_epoch, SQUARE_PAYMENT_LOOKUP_WINDOW_SECS)
.has_lightning_payment_near_timestamp(
invoice_epoch,
SQUARE_PAYMENT_LOOKUP_WINDOW_SECS,
location_ids,
)
.await?
{
tracing::debug!(
"Square payment found near invoice timestamp {invoice_epoch} (window={}s)",
SQUARE_PAYMENT_LOOKUP_WINDOW_SECS
invoice_epoch,
invoice_time = %invoice_time,
"Square payment matched — allowing melt"
);
return Ok(());
}

#[cfg(feature = "prometheus")]
metrics::AGICASH_METRICS.record_payment_not_allowed("no_square_payment_found");

tracing::info!(
"No Square payment found near invoice timestamp {invoice_epoch} (window={}s)",
SQUARE_PAYMENT_LOOKUP_WINDOW_SECS
tracing::warn!(
invoice_epoch,
invoice_time = %invoice_time,
window_secs = SQUARE_PAYMENT_LOOKUP_WINDOW_SECS,
destination = %valid_destination_name,
"REJECTED: No Square Lightning payment found in time window. \
Invoice was created at {} but no matching Square payment exists \
within +/-{}s of that timestamp.",
invoice_time,
SQUARE_PAYMENT_LOOKUP_WINDOW_SECS,
);

// Log all Lightning payments in the last 3 hours for debugging
api_client
.log_recent_lightning_payments(3, invoice_epoch, location_ids)
.await;

Err(payment::Error::PaymentNotAllowed(format!(
"This ecash can only be spent at {}",
valid_destination_name
Expand Down
Loading
Loading