Skip to main content

cdk_spilman/
bridge.rs

1#![allow(missing_docs)]
2//! Spilman Protocol Bridge
3//!
4//! This module provides a high-level bridge for implementing Spilman payment channels
5//! in any service provider. It handles the core protocol logic, validation, and
6//! signature verification, while delegating storage and pricing to a host hook.
7
8use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
9use serde::{Deserialize, Serialize};
10
11use super::params::Stage2Role;
12use super::{
13    verify_valid_channel, ChannelParameters, CommitmentOutputs, DeterministicSecretWithBlinding,
14    EstablishedChannel, KeysetInfo,
15};
16use async_trait::async_trait;
17use cashu::nuts::{BlindSignature, CurrencyUnit, Id, Proof, PublicKey, SwapRequest};
18use cashu::util::hex;
19use std::str::FromStr;
20
21/// Funding data for a channel
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ChannelFunding {
24    /// Serialized channel parameters
25    pub params_json: String,
26    /// Serialized funding proofs
27    pub funding_proofs_json: String,
28    /// Hex-encoded channel secret
29    pub channel_secret_hex: String,
30    /// Serialized keyset info
31    pub keyset_info_json: String,
32}
33
34/// Payment proof for a channel
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PaymentProof {
37    /// Current balance
38    pub balance: u64,
39    /// Alice's signature over the balance
40    pub signature: String,
41}
42
43/// Channel lifecycle states
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum ChannelState {
46    /// Channel is open and accepting payments
47    Open,
48    /// Channel is closing (swap pending, no more payments accepted)
49    Closing,
50    /// Channel is closed (swap completed, proofs stored)
51    Closed,
52}
53
54/// Data stored when a channel enters CLOSING state
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ClosingData {
57    /// The channel's expiry timestamp
58    pub expiry_timestamp: u64,
59    /// The balance at close
60    pub balance: u64,
61    /// The client's Schnorr signature authorizing this balance
62    pub signature: String,
63}
64
65/// Host hooks for the Spilman bridge
66///
67/// Implement this trait to provide storage and pricing logic for your service.
68/// The generic type `C` allows for a custom request context used in pricing.
69pub trait SpilmanHost<C = String> {
70    /// Check if the receiver pubkey in the channel params is acceptable
71    fn receiver_key_is_acceptable(&self, receiver_pubkey: &PublicKey) -> bool;
72
73    /// Check if the mint and keyset are acceptable
74    fn mint_and_keyset_is_acceptable(&self, mint: &str, keyset_id: &cashu::nuts::Id) -> bool;
75
76    /// Get fully persisted funding data for a channel.
77    ///
78    /// Return `None` while an interrupted initial save is missing its signed
79    /// initial payment so the original funding request can repair it.
80    fn get_funding(&self, channel_id: &str) -> Option<ChannelFunding>;
81
82    /// Save funding data for a channel, including the initial payment proof.
83    /// An error suppresses payment success and must cover any failed funding
84    /// or initial-balance persistence.
85    fn save_funding(
86        &self,
87        channel_id: &str,
88        funding: ChannelFunding,
89        initial_payment: PaymentProof,
90    ) -> Result<(), String>;
91
92    /// Get the current amount due for a channel
93    fn get_amount_due(&self, channel_id: &str, context: Option<&C>) -> u64;
94
95    /// Persist an accepted payment and its usage update.
96    /// Return an error if either write fails; the bridge will not acknowledge
97    /// the payment as successful.
98    fn record_payment(
99        &self,
100        channel_id: &str,
101        payment: PaymentProof,
102        context: &C,
103    ) -> Result<(), String>;
104
105    /// Get the current state of a channel.
106    fn get_channel_state(&self, channel_id: &str) -> ChannelState;
107
108    /// Mark a channel as closing (pre-swap state).
109    fn mark_channel_closing(
110        &self,
111        channel_id: &str,
112        expiry_timestamp: u64,
113        payment: PaymentProof,
114    ) -> Result<(), String>;
115
116    /// Get the stored closing data for a channel in CLOSING state.
117    fn get_closing_data(&self, channel_id: &str) -> Option<ClosingData>;
118
119    /// Get channel policy for a given unit: funding-time validation thresholds.
120    /// Returns `None` if the unit is not supported.
121    fn get_channel_policy(&self, unit: &str) -> Option<ChannelPolicy>;
122
123    /// Get the current time in seconds
124    fn now_seconds(&self) -> u64;
125
126    /// Get the balance and signature for a unilateral exit
127    fn get_balance_and_signature_for_unilateral_exit(
128        &self,
129        channel_id: &str,
130    ) -> Option<PaymentProof>;
131
132    /// Get active keyset IDs for a mint and unit
133    fn get_active_keyset_ids(&self, mint: &str, unit: &CurrencyUnit) -> Vec<Id>;
134
135    /// Get full KeysetInfo JSON for a specific keyset
136    fn get_keyset_info(&self, mint: &str, keyset_id: &Id) -> Option<String>;
137
138    /// Mark a channel as closed and persist the final state
139    #[allow(clippy::too_many_arguments)]
140    fn mark_channel_closed(
141        &self,
142        channel_id: &str,
143        expiry_timestamp: u64,
144        balance: u64,
145        receiver_proofs_json: &str,
146        sender_proofs_json: &str,
147        receiver_sum: u64,
148        sender_sum: u64,
149    ) -> Result<(), String>;
150
151    /// Compute the ECDH-derived channel secret.
152    fn compute_channel_secret(
153        &self,
154        receiver_pubkey_hex: &str,
155        sender_pubkey_hex: &str,
156    ) -> Result<String, String>;
157
158    /// Sign a message with the tweaked (P2BK-blinded) server key.
159    fn sign_with_tweaked_key(
160        &self,
161        signer_pubkey_hex: &str,
162        message_hex: &str,
163        tweak_scalar_hex: &str,
164    ) -> Result<String, String>;
165}
166
167/// Sync networking hooks for the Spilman bridge
168pub trait SpilmanNetworking {
169    /// Call the mint's /v1/swap endpoint
170    fn call_mint_swap(&self, mint_url: &str, swap_request_json: &str) -> Result<String, String>;
171
172    /// Refresh the keyset cache for a mint
173    fn refresh_all_keysets(&self, mint: &str) -> Result<(), String>;
174}
175
176/// Async networking hooks for the Spilman bridge
177#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
178#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
179pub trait SpilmanAsyncNetworking {
180    /// Call the mint's /v1/swap endpoint
181    async fn call_mint_swap(
182        &self,
183        mint_url: &str,
184        swap_request_json: &str,
185    ) -> Result<String, String>;
186
187    /// Refresh the keyset cache for a mint
188    async fn refresh_all_keysets(&self, mint: &str) -> Result<(), String>;
189}
190
191/// Bridge for processing Spilman payments
192#[derive(Debug)]
193pub struct SpilmanBridge<H: SpilmanHost<C>, C = String> {
194    host: H,
195    _phantom: std::marker::PhantomData<C>,
196}
197
198/// A signed payment for a Spilman channel.
199///
200/// This is the core protocol message exchanged between client and server.
201/// The client creates it via `SpilmanClientBridge::create_payment()`,
202/// the server validates it via `SpilmanBridge::process_payment()`.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct Payment {
205    /// Channel identifier
206    pub channel_id: String,
207    /// Cumulative balance the receiver can claim (monotonically increasing)
208    pub balance: u64,
209    /// BIP-340 Schnorr signature over the balance commitment
210    pub signature: String,
211    /// Channel parameters (required on first payment to register channel)
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub params: Option<serde_json::Value>,
214    /// Funding proofs (required on first payment to register channel)
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub funding_proofs: Option<Vec<Proof>>,
217}
218
219impl Payment {
220    /// Create a payment without funding data (for subsequent payments)
221    pub fn new(channel_id: String, balance: u64, signature: String) -> Self {
222        Self {
223            channel_id,
224            balance,
225            signature,
226            params: None,
227            funding_proofs: None,
228        }
229    }
230
231    /// Create a payment with funding data (for first payment)
232    pub fn with_funding(
233        channel_id: String,
234        balance: u64,
235        signature: String,
236        params: serde_json::Value,
237        funding_proofs: Vec<Proof>,
238    ) -> Self {
239        Self {
240            channel_id,
241            balance,
242            signature,
243            params: Some(params),
244            funding_proofs: Some(funding_proofs),
245        }
246    }
247
248    /// Check if this payment includes funding data
249    pub fn has_funding(&self) -> bool {
250        self.params.is_some() && self.funding_proofs.is_some()
251    }
252}
253
254/// Result of a successful payment
255#[derive(Debug, Clone, Serialize)]
256pub struct PaymentSuccess {
257    pub channel_id: String,
258    pub balance: u64,
259    pub amount_due: u64,
260    pub capacity: u64,
261}
262
263/// Data needed to close a channel
264#[derive(Debug)]
265pub struct CloseData {
266    pub swap_request: SwapRequest,
267    pub expected_total: u64,
268    pub secrets_with_blinding: Vec<(DeterministicSecretWithBlinding, bool)>,
269    pub output_keyset_info: KeysetInfo,
270}
271
272impl CloseData {
273    pub fn to_json_value(self) -> serde_json::Value {
274        let swap_request_json =
275            serde_json::to_value(&self.swap_request).unwrap_or(serde_json::Value::Null);
276
277        let secrets_with_blinding: Vec<serde_json::Value> = self
278            .secrets_with_blinding
279            .into_iter()
280            .map(|(s, is_receiver)| {
281                serde_json::json!({
282                    "secret": s.secret.to_string(),
283                    "blinding_factor": hex::encode(s.blinding_factor.secret_bytes()),
284                    "amount": s.amount,
285                    "index": s.index,
286                    "is_receiver": is_receiver
287                })
288            })
289            .collect();
290
291        serde_json::json!({
292            "success": true,
293            "swap_request": swap_request_json,
294            "expected_total": self.expected_total,
295            "secrets_with_blinding": secrets_with_blinding,
296            "output_keyset_info": serde_json::to_value(&self.output_keyset_info).unwrap_or(serde_json::Value::Null)
297        })
298    }
299}
300
301/// A proof with its (amount, index) metadata from the commitment outputs
302#[derive(Debug)]
303pub struct ProofWithMeta {
304    pub proof: Proof,
305    pub amount: u64,
306    pub index: usize,
307    pub is_receiver: bool,
308}
309
310/// Result of unblinding and verifying stage 1 swap response
311#[derive(Debug)]
312pub struct UnblindResult {
313    pub receiver_proofs: Vec<ProofWithMeta>,
314    pub sender_proofs: Vec<ProofWithMeta>,
315    pub receiver_sum: u64,
316    pub sender_sum: u64,
317}
318
319/// Everything needed to execute a close operation after sync validation.
320#[derive(Debug)]
321pub struct PreparedClose {
322    pub channel_id: String,
323    pub balance: u64,
324    pub mint_url: String,
325    pub swap_request: serde_json::Value,
326    pub secrets_with_blinding: serde_json::Value,
327    pub output_keyset_info: serde_json::Value,
328    pub params_json: String,
329    pub keyset_info_json: String,
330    pub channel_secret: String,
331}
332
333/// HTTP-friendly error for close preparation.
334#[derive(Debug, Clone, Serialize)]
335pub struct ClosePreparationError {
336    pub error: String,
337    pub reason: String,
338    pub status: u16,
339    #[serde(flatten)]
340    pub extra: Option<serde_json::Map<String, serde_json::Value>>,
341}
342
343impl ClosePreparationError {
344    pub fn to_json(&self) -> String {
345        serde_json::to_string(self).unwrap_or_default()
346    }
347
348    pub fn bad_request(reason: impl Into<String>) -> Self {
349        Self {
350            error: "Bad request".into(),
351            reason: reason.into(),
352            status: 400,
353            extra: None,
354        }
355    }
356
357    pub fn payment_required(reason: impl Into<String>) -> Self {
358        Self {
359            error: "Payment required".into(),
360            reason: reason.into(),
361            status: 402,
362            extra: None,
363        }
364    }
365
366    pub fn not_found(reason: impl Into<String>) -> Self {
367        let reason = reason.into();
368        Self {
369            error: reason.clone(),
370            reason,
371            status: 404,
372            extra: None,
373        }
374    }
375
376    pub fn internal(reason: impl Into<String>) -> Self {
377        Self {
378            error: "Internal error".into(),
379            reason: reason.into(),
380            status: 500,
381            extra: None,
382        }
383    }
384
385    pub fn conflict(reason: impl Into<String>) -> Self {
386        Self {
387            error: "Channel closing".into(),
388            reason: reason.into(),
389            status: 409,
390            extra: None,
391        }
392    }
393
394    pub fn gone(reason: impl Into<String>) -> Self {
395        Self {
396            error: "Channel closed".into(),
397            reason: reason.into(),
398            status: 410,
399            extra: None,
400        }
401    }
402
403    pub fn with_extra(mut self, extra: serde_json::Map<String, serde_json::Value>) -> Self {
404        self.extra = Some(extra);
405        self
406    }
407
408    pub fn from_bridge_error(err: BridgeError) -> Self {
409        let reason = err.to_string();
410        match &err {
411            BridgeError::ChannelClosed => Self::gone(reason),
412            BridgeError::ChannelClosing => Self::conflict(reason),
413            BridgeError::UnknownChannel => Self::not_found(reason),
414            BridgeError::InvalidRequest(msg) if msg.contains("no payment proof") => {
415                Self::bad_request(reason)
416            }
417            BridgeError::Internal(_) | BridgeError::ServerMisconfigured(_) => {
418                Self::internal(reason)
419            }
420            BridgeError::BalanceMismatch { expected, actual } => {
421                let mut extra = serde_json::Map::new();
422                extra.insert("expected".into(), serde_json::json!(expected));
423                extra.insert("actual".into(), serde_json::json!(actual));
424                Self::payment_required(reason).with_extra(extra)
425            }
426            _ => Self::payment_required(reason),
427        }
428    }
429}
430
431/// HTTP-friendly error for payment/validation failures.
432#[derive(Debug, Clone, Serialize)]
433pub struct BridgeErrorResponse {
434    pub error: String,
435    pub reason: String,
436    pub status: u16,
437    pub code: String,
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub extra: Option<serde_json::Map<String, serde_json::Value>>,
440}
441
442impl BridgeErrorResponse {
443    pub fn from_bridge_error(err: &BridgeError) -> Self {
444        let reason = err.to_string();
445        let mut extra: Option<serde_json::Map<String, serde_json::Value>> = None;
446
447        let (status, error, code) = match err {
448            BridgeError::InvalidRequest(_) => (400, "Bad request", "invalid_request"),
449            BridgeError::UnknownChannel => (404, "Not found", "unknown_channel"),
450            BridgeError::ChannelClosing => (409, "Channel closing", "channel_closing"),
451            BridgeError::ChannelClosed => (410, "Channel closed", "channel_closed"),
452            BridgeError::ServerMisconfigured(_) => (500, "Internal error", "server_misconfigured"),
453            BridgeError::Internal(_) => (500, "Internal error", "internal"),
454            BridgeError::BalanceMismatch { expected, actual } => {
455                let mut map = serde_json::Map::new();
456                map.insert("expected".into(), serde_json::json!(expected));
457                map.insert("actual".into(), serde_json::json!(actual));
458                extra = Some(map);
459                (402, "Payment required", "balance_mismatch")
460            }
461            BridgeError::BalanceExceedsCapacity { balance, capacity } => {
462                let mut map = serde_json::Map::new();
463                map.insert("balance".into(), serde_json::json!(balance));
464                map.insert("capacity".into(), serde_json::json!(capacity));
465                extra = Some(map);
466                (402, "Payment required", "balance_exceeds_capacity")
467            }
468            BridgeError::InsufficientBalance {
469                balance,
470                amount_due,
471            } => {
472                let mut map = serde_json::Map::new();
473                map.insert("balance".into(), serde_json::json!(balance));
474                map.insert("amount_due".into(), serde_json::json!(amount_due));
475                extra = Some(map);
476                (402, "Payment required", "insufficient_balance")
477            }
478            BridgeError::CapacityTooSmall {
479                capacity,
480                min_capacity,
481            } => {
482                let mut map = serde_json::Map::new();
483                map.insert("capacity".into(), serde_json::json!(capacity));
484                map.insert("min_capacity".into(), serde_json::json!(min_capacity));
485                extra = Some(map);
486                (402, "Payment required", "capacity_too_small")
487            }
488            BridgeError::ExpiryTooSoon {
489                expiry_timestamp,
490                min_expiry,
491                now,
492            } => {
493                let mut map = serde_json::Map::new();
494                map.insert(
495                    "expiry_timestamp".into(),
496                    serde_json::json!(expiry_timestamp),
497                );
498                map.insert("min_expiry".into(), serde_json::json!(min_expiry));
499                map.insert("now".into(), serde_json::json!(now));
500                extra = Some(map);
501                (402, "Payment required", "expiry_too_soon")
502            }
503            BridgeError::MaxAmountExceeded {
504                amount,
505                max_allowed,
506            } => {
507                let mut map = serde_json::Map::new();
508                map.insert("amount".into(), serde_json::json!(amount));
509                map.insert("max_allowed".into(), serde_json::json!(max_allowed));
510                extra = Some(map);
511                (402, "Payment required", "max_amount_exceeded")
512            }
513            BridgeError::UnsupportedUnit(_) => (402, "Payment required", "unsupported_unit"),
514            BridgeError::ChannelIdMismatch => (402, "Payment required", "channel_id_mismatch"),
515            BridgeError::ValidationFailed(_) => (402, "Payment required", "validation_failed"),
516            BridgeError::InvalidSignature(_) => (402, "Payment required", "invalid_signature"),
517            BridgeError::ReceiverKeyNotAcceptable => {
518                (402, "Payment required", "receiver_key_not_acceptable")
519            }
520            BridgeError::MintOrKeysetNotAcceptable => {
521                (402, "Payment required", "mint_or_keyset_not_acceptable")
522            }
523        };
524
525        Self {
526            error: error.to_string(),
527            reason,
528            status,
529            code: code.to_string(),
530            extra,
531        }
532    }
533
534    pub fn to_json(&self) -> String {
535        serde_json::to_string(self).unwrap_or_else(|_| {
536            "{\"error\":\"Internal error\",\"reason\":\"failed to serialize bridge error\",\"status\":500,\"code\":\"internal\"}"
537                .to_string()
538        })
539    }
540}
541
542#[derive(Debug, Clone, Serialize)]
543pub struct PaymentValidationResult {
544    pub channel_id: String,
545    pub balance: u64,
546    pub amount_due: u64,
547    pub capacity: u64,
548    pub sender_signature: String,
549}
550
551#[derive(Debug, Clone, Serialize)]
552pub struct FundChannelResult {
553    pub channel_id: String,
554    pub capacity: u64,
555    pub already_known: bool,
556}
557
558#[derive(Debug, Clone, Serialize)]
559pub struct CloseSuccess {
560    pub channel_id: String,
561    pub total_value: u64,
562    pub receiver_sum: u64,
563    pub sender_sum: u64,
564    pub sender_proofs: String,
565    pub already_closed: bool,
566}
567
568#[derive(Debug, Clone, Serialize)]
569#[serde(tag = "type")]
570pub enum CloseError {
571    #[serde(rename = "validation_failed")]
572    ValidationFailed {
573        reason: String,
574        status: u16,
575        #[serde(skip_serializing_if = "Option::is_none")]
576        expected_balance: Option<u64>,
577        #[serde(skip_serializing_if = "Option::is_none")]
578        actual_balance: Option<u64>,
579    },
580    #[serde(rename = "unknown_channel")]
581    UnknownChannel { status: u16 },
582    #[serde(rename = "already_closed")]
583    AlreadyClosed {
584        closed_balance: u64,
585        requested_balance: u64,
586        status: u16,
587    },
588    #[serde(rename = "mint_rejected")]
589    MintRejected {
590        mint_error: serde_json::Value,
591        status: u16,
592    },
593    #[serde(rename = "mint_rejected_after_retry")]
594    MintRejectedAfterRetry {
595        original_error: serde_json::Value,
596        retry_error: serde_json::Value,
597        status: u16,
598    },
599    #[serde(rename = "unblind_failed")]
600    UnblindFailed { reason: String, status: u16 },
601    #[serde(rename = "storage_failed")]
602    StorageFailed { reason: String, status: u16 },
603}
604
605impl CloseError {
606    pub fn status_code(&self) -> u16 {
607        match self {
608            Self::ValidationFailed { status, .. }
609            | Self::UnknownChannel { status }
610            | Self::AlreadyClosed { status, .. }
611            | Self::MintRejected { status, .. }
612            | Self::MintRejectedAfterRetry { status, .. }
613            | Self::UnblindFailed { status, .. }
614            | Self::StorageFailed { status, .. } => *status,
615        }
616    }
617
618    pub fn from_preparation_error(err: ClosePreparationError) -> Self {
619        let (expected_balance, actual_balance) = if let Some(extra) = &err.extra {
620            (
621                extra.get("expected").and_then(|v| v.as_u64()),
622                extra.get("actual").and_then(|v| v.as_u64()),
623            )
624        } else {
625            (None, None)
626        };
627        Self::ValidationFailed {
628            reason: err.reason,
629            status: err.status,
630            expected_balance,
631            actual_balance,
632        }
633    }
634
635    pub fn unknown_channel() -> Self {
636        Self::UnknownChannel { status: 404 }
637    }
638    pub fn mint_rejected(mint_error: serde_json::Value) -> Self {
639        Self::MintRejected {
640            mint_error,
641            status: 502,
642        }
643    }
644    pub fn mint_rejected_after_retry(
645        original_error: serde_json::Value,
646        retry_error: serde_json::Value,
647    ) -> Self {
648        Self::MintRejectedAfterRetry {
649            original_error,
650            retry_error,
651            status: 502,
652        }
653    }
654    pub fn unblind_failed(reason: impl Into<String>) -> Self {
655        Self::UnblindFailed {
656            reason: reason.into(),
657            status: 500,
658        }
659    }
660    pub fn storage_failed(reason: impl Into<String>) -> Self {
661        Self::StorageFailed {
662            reason: reason.into(),
663            status: 500,
664        }
665    }
666}
667
668fn parse_mint_error_value(raw: &str) -> serde_json::Value {
669    serde_json::from_str(raw).unwrap_or_else(|_| serde_json::Value::String(raw.to_string()))
670}
671
672/// Extract the NUT-00 error code from a raw error string.
673/// Returns None if the string is not valid JSON or lacks a "code" field.
674fn extract_nut00_error_code(raw: &str) -> Option<u32> {
675    serde_json::from_str::<serde_json::Value>(raw)
676        .ok()
677        .and_then(|v| v.get("code")?.as_u64())
678        .map(|c| c as u32)
679}
680
681/// Returns true if the error code is in the keyset error range (12xxx).
682/// These errors may be recoverable by refreshing keysets and retrying.
683///
684/// Workaround: also treats code 99999 as retryable. Nutmix returns this
685/// catch-all code instead of the spec-standard 12001 ("Keyset is not known").
686/// This can be removed once nutmix is fixed:
687/// <https://github.com/lescuer97/nutmix/issues/237>
688fn is_keyset_error_code(code: u32) -> bool {
689    (12000..13000).contains(&code) || code == 99999
690}
691
692/// Determine if a swap error should trigger a retry (refresh keysets + re-attempt).
693/// Keyset errors (12xxx) and code 99999 (nutmix workaround) are retryable.
694/// All other errors fail immediately. If the error can't be parsed, fail immediately.
695fn should_retry_swap_error(raw: &str) -> bool {
696    match extract_nut00_error_code(raw) {
697        Some(code) => {
698            let retryable = is_keyset_error_code(code);
699            if retryable {
700                tracing::debug!(
701                    code,
702                    "Keyset error detected, will retry after refreshing keysets"
703                );
704            } else {
705                tracing::debug!(code, "Non-retryable NUT-00 error code, failing immediately");
706            }
707            retryable
708        }
709        None => {
710            tracing::debug!(error = %raw, "Could not parse NUT-00 error code, failing immediately");
711            false
712        }
713    }
714}
715
716impl std::fmt::Display for CloseError {
717    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718        match self {
719            Self::ValidationFailed { reason, .. } => write!(f, "validation failed: {}", reason),
720            Self::UnknownChannel { .. } => write!(f, "unknown channel"),
721            Self::AlreadyClosed {
722                closed_balance,
723                requested_balance,
724                ..
725            } => write!(
726                f,
727                "channel already closed with balance {} (requested {})",
728                closed_balance, requested_balance
729            ),
730            Self::MintRejected { mint_error, .. } => {
731                write!(f, "mint rejected swap: {}", mint_error)
732            }
733            Self::MintRejectedAfterRetry {
734                original_error,
735                retry_error,
736                ..
737            } => write!(
738                f,
739                "mint rejected swap after retry: original={}, retry={}",
740                original_error, retry_error
741            ),
742            Self::UnblindFailed { reason, .. } => write!(f, "unblind failed: {}", reason),
743            Self::StorageFailed { reason, .. } => write!(f, "storage failed: {}", reason),
744        }
745    }
746}
747
748impl std::error::Error for CloseError {}
749
750/// Funding-time validation thresholds for a given unit, returned by
751/// [`SpilmanHost::get_channel_policy`].
752#[derive(Debug, Clone)]
753pub struct ChannelPolicy {
754    /// Minimum seconds between now and the channel expiry timestamp.
755    pub min_expiry_in_seconds: u64,
756    /// Minimum channel capacity (in the unit's base denomination).
757    pub min_capacity: u64,
758    /// Optional cap on the largest single proof denomination.
759    pub max_amount_per_output: Option<u64>,
760}
761
762#[derive(Debug)]
763pub enum BridgeError {
764    InvalidRequest(String),
765    ChannelClosed,
766    ChannelClosing,
767    ServerMisconfigured(String),
768    CapacityTooSmall {
769        capacity: u64,
770        min_capacity: u64,
771    },
772    ExpiryTooSoon {
773        expiry_timestamp: u64,
774        min_expiry: u64,
775        now: u64,
776    },
777    MaxAmountExceeded {
778        amount: u64,
779        max_allowed: u64,
780    },
781    BalanceExceedsCapacity {
782        balance: u64,
783        capacity: u64,
784    },
785    UnsupportedUnit(String),
786    ChannelIdMismatch,
787    ValidationFailed(String),
788    UnknownChannel,
789    InvalidSignature(String),
790    InsufficientBalance {
791        balance: u64,
792        amount_due: u64,
793    },
794    BalanceMismatch {
795        expected: u64,
796        actual: u64,
797    },
798    Internal(String),
799    ReceiverKeyNotAcceptable,
800    MintOrKeysetNotAcceptable,
801}
802
803impl std::fmt::Display for BridgeError {
804    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
805        match self {
806            Self::InvalidRequest(s) => write!(f, "{}", s),
807            Self::ChannelClosed => write!(f, "channel closed"),
808            Self::ChannelClosing => write!(f, "channel closing, swap pending"),
809            Self::ServerMisconfigured(s) => write!(f, "server misconfigured: {}", s),
810            Self::CapacityTooSmall {
811                capacity,
812                min_capacity,
813            } => write!(f, "capacity too small: {} < {}", capacity, min_capacity),
814            Self::ExpiryTooSoon {
815                expiry_timestamp,
816                min_expiry,
817                now,
818            } => write!(
819                f,
820                "expiry too soon: {} < {} ({}s remaining)",
821                expiry_timestamp,
822                min_expiry,
823                expiry_timestamp.saturating_sub(*now)
824            ),
825            Self::MaxAmountExceeded {
826                amount,
827                max_allowed,
828            } => write!(
829                f,
830                "max_amount_per_output exceeded: {} > {}",
831                amount, max_allowed
832            ),
833            Self::BalanceExceedsCapacity { balance, capacity } => {
834                write!(f, "balance exceeds capacity: {} > {}", balance, capacity)
835            }
836            Self::UnsupportedUnit(u) => write!(f, "unsupported unit: {}", u),
837            Self::ChannelIdMismatch => write!(f, "channel_id mismatch"),
838            Self::ValidationFailed(s) => write!(f, "channel validation failed: {}", s),
839            Self::UnknownChannel => write!(f, "unknown channel"),
840            Self::InvalidSignature(s) => write!(f, "invalid signature: {}", s),
841            Self::InsufficientBalance {
842                balance,
843                amount_due,
844            } => write!(f, "insufficient balance: {} < {}", balance, amount_due),
845            Self::BalanceMismatch { expected, actual } => {
846                write!(f, "balance mismatch: expected {}, got {}", expected, actual)
847            }
848            Self::Internal(s) => write!(f, "internal error: {}", s),
849            Self::ReceiverKeyNotAcceptable => write!(f, "receiver key not acceptable"),
850            Self::MintOrKeysetNotAcceptable => write!(f, "mint or keyset not acceptable"),
851        }
852    }
853}
854
855impl BridgeError {
856    pub fn to_response(&self) -> BridgeErrorResponse {
857        BridgeErrorResponse::from_bridge_error(self)
858    }
859
860    pub fn to_response_json(&self) -> String {
861        self.to_response().to_json()
862    }
863}
864
865pub fn unblind_and_verify_stage1_response(
866    blind_signatures: Vec<BlindSignature>,
867    secrets_with_blinding: Vec<(DeterministicSecretWithBlinding, bool)>,
868    params: &ChannelParameters,
869    output_keyset_info: &KeysetInfo,
870    balance: u64,
871) -> Result<UnblindResult, BridgeError> {
872    if blind_signatures.len() != secrets_with_blinding.len() {
873        return Err(BridgeError::Internal(
874            "Length mismatch between signatures and secrets".into(),
875        ));
876    }
877    let mut secrets = Vec::with_capacity(secrets_with_blinding.len());
878    let mut blinding_factors = Vec::with_capacity(secrets_with_blinding.len());
879    let mut is_receiver_flags = Vec::with_capacity(secrets_with_blinding.len());
880    let mut amount_index_pairs = Vec::with_capacity(secrets_with_blinding.len());
881
882    for (swb, is_receiver) in secrets_with_blinding {
883        secrets.push(swb.secret);
884        blinding_factors.push(swb.blinding_factor);
885        is_receiver_flags.push(is_receiver);
886        amount_index_pairs.push((swb.amount, swb.index));
887    }
888
889    let proofs = cashu::dhke::construct_proofs(
890        blind_signatures,
891        blinding_factors,
892        secrets,
893        &output_keyset_info.active_keys,
894    )
895    .map_err(|e| BridgeError::Internal(format!("Failed to construct proofs: {}", e)))?;
896
897    for (i, proof) in proofs.iter().enumerate() {
898        let mint_pubkey = output_keyset_info
899            .active_keys
900            .amount_key(proof.amount)
901            .ok_or_else(|| BridgeError::Internal("Missing mint key".into()))?;
902        proof.verify_dleq(mint_pubkey).map_err(|e| {
903            BridgeError::ValidationFailed(format!("DLEQ failed for proof {}: {}", i, e))
904        })?;
905    }
906
907    let mut receiver_proofs = Vec::new();
908    let mut sender_proofs = Vec::new();
909    let mut receiver_sum = 0;
910    let mut sender_sum = 0;
911
912    for ((mut proof, is_receiver), (amount, index)) in proofs
913        .into_iter()
914        .zip(is_receiver_flags)
915        .zip(amount_index_pairs)
916    {
917        let role = if is_receiver {
918            Stage2Role::Receiver
919        } else {
920            Stage2Role::Sender
921        };
922        params
923            .attach_stage2_p2pk_e(&mut proof, role, amount, index)
924            .map_err(|e| BridgeError::Internal(e.to_string()))?;
925
926        if is_receiver {
927            let expected_pubkey = params
928                .get_receiver_blinded_pubkey_for_stage2_output(amount, index)
929                .map_err(|e| BridgeError::Internal(e.to_string()))?;
930            let secret_json: serde_json::Value = serde_json::from_str(&proof.secret.to_string())
931                .map_err(|e| BridgeError::Internal(e.to_string()))?;
932            if secret_json.get(0).and_then(|v| v.as_str()) != Some("P2PK")
933                || secret_json
934                    .get(1)
935                    .and_then(|v| v.get("data"))
936                    .and_then(|v| v.as_str())
937                    != Some(&expected_pubkey.to_hex())
938            {
939                return Err(BridgeError::ValidationFailed(
940                    "Receiver proof locked to wrong pubkey".into(),
941                ));
942            }
943            receiver_sum += u64::from(proof.amount);
944            receiver_proofs.push(ProofWithMeta {
945                proof,
946                amount,
947                index,
948                is_receiver: true,
949            });
950        } else {
951            sender_sum += u64::from(proof.amount);
952            sender_proofs.push(ProofWithMeta {
953                proof,
954                amount,
955                index,
956                is_receiver: false,
957            });
958        }
959    }
960
961    let expected_nominal = output_keyset_info
962        .inverse_deterministic_value_after_fees(balance, params.maximum_amount_for_one_output)
963        .map_err(|e| BridgeError::Internal(e.to_string()))?
964        .nominal_value;
965    if receiver_sum != expected_nominal {
966        return Err(BridgeError::ValidationFailed(format!(
967            "Receiver nominal mismatch: expected {}, got {}",
968            expected_nominal, receiver_sum
969        )));
970    }
971
972    Ok(UnblindResult {
973        receiver_proofs,
974        sender_proofs,
975        receiver_sum,
976        sender_sum,
977    })
978}
979
980mod close;
981mod payment;
982
983#[cfg(test)]
984mod tests;
985
986#[cfg(test)]
987#[path = "bridge_persistence_tests.rs"]
988mod persistence_tests;