1#![allow(missing_docs)]
2use 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#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ChannelFunding {
24 pub params_json: String,
26 pub funding_proofs_json: String,
28 pub channel_secret_hex: String,
30 pub keyset_info_json: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PaymentProof {
37 pub balance: u64,
39 pub signature: String,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum ChannelState {
46 Open,
48 Closing,
50 Closed,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ClosingData {
57 pub expiry_timestamp: u64,
59 pub balance: u64,
61 pub signature: String,
63}
64
65pub trait SpilmanHost<C = String> {
70 fn receiver_key_is_acceptable(&self, receiver_pubkey: &PublicKey) -> bool;
72
73 fn mint_and_keyset_is_acceptable(&self, mint: &str, keyset_id: &cashu::nuts::Id) -> bool;
75
76 fn get_funding(&self, channel_id: &str) -> Option<ChannelFunding>;
81
82 fn save_funding(
86 &self,
87 channel_id: &str,
88 funding: ChannelFunding,
89 initial_payment: PaymentProof,
90 ) -> Result<(), String>;
91
92 fn get_amount_due(&self, channel_id: &str, context: Option<&C>) -> u64;
94
95 fn record_payment(
99 &self,
100 channel_id: &str,
101 payment: PaymentProof,
102 context: &C,
103 ) -> Result<(), String>;
104
105 fn get_channel_state(&self, channel_id: &str) -> ChannelState;
107
108 fn mark_channel_closing(
110 &self,
111 channel_id: &str,
112 expiry_timestamp: u64,
113 payment: PaymentProof,
114 ) -> Result<(), String>;
115
116 fn get_closing_data(&self, channel_id: &str) -> Option<ClosingData>;
118
119 fn get_channel_policy(&self, unit: &str) -> Option<ChannelPolicy>;
122
123 fn now_seconds(&self) -> u64;
125
126 fn get_balance_and_signature_for_unilateral_exit(
128 &self,
129 channel_id: &str,
130 ) -> Option<PaymentProof>;
131
132 fn get_active_keyset_ids(&self, mint: &str, unit: &CurrencyUnit) -> Vec<Id>;
134
135 fn get_keyset_info(&self, mint: &str, keyset_id: &Id) -> Option<String>;
137
138 #[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 fn compute_channel_secret(
153 &self,
154 receiver_pubkey_hex: &str,
155 sender_pubkey_hex: &str,
156 ) -> Result<String, String>;
157
158 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
167pub trait SpilmanNetworking {
169 fn call_mint_swap(&self, mint_url: &str, swap_request_json: &str) -> Result<String, String>;
171
172 fn refresh_all_keysets(&self, mint: &str) -> Result<(), String>;
174}
175
176#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
178#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
179pub trait SpilmanAsyncNetworking {
180 async fn call_mint_swap(
182 &self,
183 mint_url: &str,
184 swap_request_json: &str,
185 ) -> Result<String, String>;
186
187 async fn refresh_all_keysets(&self, mint: &str) -> Result<(), String>;
189}
190
191#[derive(Debug)]
193pub struct SpilmanBridge<H: SpilmanHost<C>, C = String> {
194 host: H,
195 _phantom: std::marker::PhantomData<C>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct Payment {
205 pub channel_id: String,
207 pub balance: u64,
209 pub signature: String,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub params: Option<serde_json::Value>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub funding_proofs: Option<Vec<Proof>>,
217}
218
219impl Payment {
220 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 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 pub fn has_funding(&self) -> bool {
250 self.params.is_some() && self.funding_proofs.is_some()
251 }
252}
253
254#[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#[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#[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#[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#[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#[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#[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
672fn 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
681fn is_keyset_error_code(code: u32) -> bool {
689 (12000..13000).contains(&code) || code == 99999
690}
691
692fn 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#[derive(Debug, Clone)]
753pub struct ChannelPolicy {
754 pub min_expiry_in_seconds: u64,
756 pub min_capacity: u64,
758 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;