#![allow(missing_docs)]
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use serde::{Deserialize, Serialize};
use super::params::Stage2Role;
use super::{
verify_valid_channel, ChannelParameters, CommitmentOutputs, DeterministicSecretWithBlinding,
EstablishedChannel, KeysetInfo,
};
use async_trait::async_trait;
use cashu::nuts::{BlindSignature, CurrencyUnit, Id, Proof, PublicKey, SwapRequest};
use cashu::util::hex;
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelFunding {
pub params_json: String,
pub funding_proofs_json: String,
pub channel_secret_hex: String,
pub keyset_info_json: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentProof {
pub balance: u64,
pub signature: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelState {
Open,
Closing,
Closed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClosingData {
pub expiry_timestamp: u64,
pub balance: u64,
pub signature: String,
}
pub trait SpilmanHost<C = String> {
fn receiver_key_is_acceptable(&self, receiver_pubkey: &PublicKey) -> bool;
fn mint_and_keyset_is_acceptable(&self, mint: &str, keyset_id: &cashu::nuts::Id) -> bool;
fn get_funding(&self, channel_id: &str) -> Option<ChannelFunding>;
fn save_funding(
&self,
channel_id: &str,
funding: ChannelFunding,
initial_payment: PaymentProof,
) -> Result<(), String>;
fn get_amount_due(&self, channel_id: &str, context: Option<&C>) -> u64;
fn record_payment(
&self,
channel_id: &str,
payment: PaymentProof,
context: &C,
) -> Result<(), String>;
fn get_channel_state(&self, channel_id: &str) -> ChannelState;
fn mark_channel_closing(
&self,
channel_id: &str,
expiry_timestamp: u64,
payment: PaymentProof,
) -> Result<(), String>;
fn get_closing_data(&self, channel_id: &str) -> Option<ClosingData>;
fn get_channel_policy(&self, unit: &str) -> Option<ChannelPolicy>;
fn now_seconds(&self) -> u64;
fn get_balance_and_signature_for_unilateral_exit(
&self,
channel_id: &str,
) -> Option<PaymentProof>;
fn get_active_keyset_ids(&self, mint: &str, unit: &CurrencyUnit) -> Vec<Id>;
fn get_keyset_info(&self, mint: &str, keyset_id: &Id) -> Option<String>;
#[allow(clippy::too_many_arguments)]
fn mark_channel_closed(
&self,
channel_id: &str,
expiry_timestamp: u64,
balance: u64,
receiver_proofs_json: &str,
sender_proofs_json: &str,
receiver_sum: u64,
sender_sum: u64,
) -> Result<(), String>;
fn compute_channel_secret(
&self,
receiver_pubkey_hex: &str,
sender_pubkey_hex: &str,
) -> Result<String, String>;
fn sign_with_tweaked_key(
&self,
signer_pubkey_hex: &str,
message_hex: &str,
tweak_scalar_hex: &str,
) -> Result<String, String>;
}
pub trait SpilmanNetworking {
fn call_mint_swap(&self, mint_url: &str, swap_request_json: &str) -> Result<String, String>;
fn refresh_all_keysets(&self, mint: &str) -> Result<(), String>;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait SpilmanAsyncNetworking {
async fn call_mint_swap(
&self,
mint_url: &str,
swap_request_json: &str,
) -> Result<String, String>;
async fn refresh_all_keysets(&self, mint: &str) -> Result<(), String>;
}
#[derive(Debug)]
pub struct SpilmanBridge<H: SpilmanHost<C>, C = String> {
host: H,
_phantom: std::marker::PhantomData<C>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Payment {
pub channel_id: String,
pub balance: u64,
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub funding_proofs: Option<Vec<Proof>>,
}
impl Payment {
pub fn new(channel_id: String, balance: u64, signature: String) -> Self {
Self {
channel_id,
balance,
signature,
params: None,
funding_proofs: None,
}
}
pub fn with_funding(
channel_id: String,
balance: u64,
signature: String,
params: serde_json::Value,
funding_proofs: Vec<Proof>,
) -> Self {
Self {
channel_id,
balance,
signature,
params: Some(params),
funding_proofs: Some(funding_proofs),
}
}
pub fn has_funding(&self) -> bool {
self.params.is_some() && self.funding_proofs.is_some()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PaymentSuccess {
pub channel_id: String,
pub balance: u64,
pub amount_due: u64,
pub capacity: u64,
}
#[derive(Debug)]
pub struct CloseData {
pub swap_request: SwapRequest,
pub expected_total: u64,
pub secrets_with_blinding: Vec<(DeterministicSecretWithBlinding, bool)>,
pub output_keyset_info: KeysetInfo,
}
impl CloseData {
pub fn to_json_value(self) -> serde_json::Value {
let swap_request_json =
serde_json::to_value(&self.swap_request).unwrap_or(serde_json::Value::Null);
let secrets_with_blinding: Vec<serde_json::Value> = self
.secrets_with_blinding
.into_iter()
.map(|(s, is_receiver)| {
serde_json::json!({
"secret": s.secret.to_string(),
"blinding_factor": hex::encode(s.blinding_factor.secret_bytes()),
"amount": s.amount,
"index": s.index,
"is_receiver": is_receiver
})
})
.collect();
serde_json::json!({
"success": true,
"swap_request": swap_request_json,
"expected_total": self.expected_total,
"secrets_with_blinding": secrets_with_blinding,
"output_keyset_info": serde_json::to_value(&self.output_keyset_info).unwrap_or(serde_json::Value::Null)
})
}
}
#[derive(Debug)]
pub struct ProofWithMeta {
pub proof: Proof,
pub amount: u64,
pub index: usize,
pub is_receiver: bool,
}
#[derive(Debug)]
pub struct UnblindResult {
pub receiver_proofs: Vec<ProofWithMeta>,
pub sender_proofs: Vec<ProofWithMeta>,
pub receiver_sum: u64,
pub sender_sum: u64,
}
#[derive(Debug)]
pub struct PreparedClose {
pub channel_id: String,
pub balance: u64,
pub mint_url: String,
pub swap_request: serde_json::Value,
pub secrets_with_blinding: serde_json::Value,
pub output_keyset_info: serde_json::Value,
pub params_json: String,
pub keyset_info_json: String,
pub channel_secret: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ClosePreparationError {
pub error: String,
pub reason: String,
pub status: u16,
#[serde(flatten)]
pub extra: Option<serde_json::Map<String, serde_json::Value>>,
}
impl ClosePreparationError {
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
pub fn bad_request(reason: impl Into<String>) -> Self {
Self {
error: "Bad request".into(),
reason: reason.into(),
status: 400,
extra: None,
}
}
pub fn payment_required(reason: impl Into<String>) -> Self {
Self {
error: "Payment required".into(),
reason: reason.into(),
status: 402,
extra: None,
}
}
pub fn not_found(reason: impl Into<String>) -> Self {
let reason = reason.into();
Self {
error: reason.clone(),
reason,
status: 404,
extra: None,
}
}
pub fn internal(reason: impl Into<String>) -> Self {
Self {
error: "Internal error".into(),
reason: reason.into(),
status: 500,
extra: None,
}
}
pub fn conflict(reason: impl Into<String>) -> Self {
Self {
error: "Channel closing".into(),
reason: reason.into(),
status: 409,
extra: None,
}
}
pub fn gone(reason: impl Into<String>) -> Self {
Self {
error: "Channel closed".into(),
reason: reason.into(),
status: 410,
extra: None,
}
}
pub fn with_extra(mut self, extra: serde_json::Map<String, serde_json::Value>) -> Self {
self.extra = Some(extra);
self
}
pub fn from_bridge_error(err: BridgeError) -> Self {
let reason = err.to_string();
match &err {
BridgeError::ChannelClosed => Self::gone(reason),
BridgeError::ChannelClosing => Self::conflict(reason),
BridgeError::UnknownChannel => Self::not_found(reason),
BridgeError::InvalidRequest(msg) if msg.contains("no payment proof") => {
Self::bad_request(reason)
}
BridgeError::Internal(_) | BridgeError::ServerMisconfigured(_) => {
Self::internal(reason)
}
BridgeError::BalanceMismatch { expected, actual } => {
let mut extra = serde_json::Map::new();
extra.insert("expected".into(), serde_json::json!(expected));
extra.insert("actual".into(), serde_json::json!(actual));
Self::payment_required(reason).with_extra(extra)
}
_ => Self::payment_required(reason),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct BridgeErrorResponse {
pub error: String,
pub reason: String,
pub status: u16,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<serde_json::Map<String, serde_json::Value>>,
}
impl BridgeErrorResponse {
pub fn from_bridge_error(err: &BridgeError) -> Self {
let reason = err.to_string();
let mut extra: Option<serde_json::Map<String, serde_json::Value>> = None;
let (status, error, code) = match err {
BridgeError::InvalidRequest(_) => (400, "Bad request", "invalid_request"),
BridgeError::UnknownChannel => (404, "Not found", "unknown_channel"),
BridgeError::ChannelClosing => (409, "Channel closing", "channel_closing"),
BridgeError::ChannelClosed => (410, "Channel closed", "channel_closed"),
BridgeError::ServerMisconfigured(_) => (500, "Internal error", "server_misconfigured"),
BridgeError::Internal(_) => (500, "Internal error", "internal"),
BridgeError::BalanceMismatch { expected, actual } => {
let mut map = serde_json::Map::new();
map.insert("expected".into(), serde_json::json!(expected));
map.insert("actual".into(), serde_json::json!(actual));
extra = Some(map);
(402, "Payment required", "balance_mismatch")
}
BridgeError::BalanceExceedsCapacity { balance, capacity } => {
let mut map = serde_json::Map::new();
map.insert("balance".into(), serde_json::json!(balance));
map.insert("capacity".into(), serde_json::json!(capacity));
extra = Some(map);
(402, "Payment required", "balance_exceeds_capacity")
}
BridgeError::InsufficientBalance {
balance,
amount_due,
} => {
let mut map = serde_json::Map::new();
map.insert("balance".into(), serde_json::json!(balance));
map.insert("amount_due".into(), serde_json::json!(amount_due));
extra = Some(map);
(402, "Payment required", "insufficient_balance")
}
BridgeError::CapacityTooSmall {
capacity,
min_capacity,
} => {
let mut map = serde_json::Map::new();
map.insert("capacity".into(), serde_json::json!(capacity));
map.insert("min_capacity".into(), serde_json::json!(min_capacity));
extra = Some(map);
(402, "Payment required", "capacity_too_small")
}
BridgeError::ExpiryTooSoon {
expiry_timestamp,
min_expiry,
now,
} => {
let mut map = serde_json::Map::new();
map.insert(
"expiry_timestamp".into(),
serde_json::json!(expiry_timestamp),
);
map.insert("min_expiry".into(), serde_json::json!(min_expiry));
map.insert("now".into(), serde_json::json!(now));
extra = Some(map);
(402, "Payment required", "expiry_too_soon")
}
BridgeError::MaxAmountExceeded {
amount,
max_allowed,
} => {
let mut map = serde_json::Map::new();
map.insert("amount".into(), serde_json::json!(amount));
map.insert("max_allowed".into(), serde_json::json!(max_allowed));
extra = Some(map);
(402, "Payment required", "max_amount_exceeded")
}
BridgeError::UnsupportedUnit(_) => (402, "Payment required", "unsupported_unit"),
BridgeError::ChannelIdMismatch => (402, "Payment required", "channel_id_mismatch"),
BridgeError::ValidationFailed(_) => (402, "Payment required", "validation_failed"),
BridgeError::InvalidSignature(_) => (402, "Payment required", "invalid_signature"),
BridgeError::ReceiverKeyNotAcceptable => {
(402, "Payment required", "receiver_key_not_acceptable")
}
BridgeError::MintOrKeysetNotAcceptable => {
(402, "Payment required", "mint_or_keyset_not_acceptable")
}
};
Self {
error: error.to_string(),
reason,
status,
code: code.to_string(),
extra,
}
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| {
"{\"error\":\"Internal error\",\"reason\":\"failed to serialize bridge error\",\"status\":500,\"code\":\"internal\"}"
.to_string()
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PaymentValidationResult {
pub channel_id: String,
pub balance: u64,
pub amount_due: u64,
pub capacity: u64,
pub sender_signature: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct FundChannelResult {
pub channel_id: String,
pub capacity: u64,
pub already_known: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct CloseSuccess {
pub channel_id: String,
pub total_value: u64,
pub receiver_sum: u64,
pub sender_sum: u64,
pub sender_proofs: String,
pub already_closed: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum CloseError {
#[serde(rename = "validation_failed")]
ValidationFailed {
reason: String,
status: u16,
#[serde(skip_serializing_if = "Option::is_none")]
expected_balance: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
actual_balance: Option<u64>,
},
#[serde(rename = "unknown_channel")]
UnknownChannel { status: u16 },
#[serde(rename = "already_closed")]
AlreadyClosed {
closed_balance: u64,
requested_balance: u64,
status: u16,
},
#[serde(rename = "mint_rejected")]
MintRejected {
mint_error: serde_json::Value,
status: u16,
},
#[serde(rename = "mint_rejected_after_retry")]
MintRejectedAfterRetry {
original_error: serde_json::Value,
retry_error: serde_json::Value,
status: u16,
},
#[serde(rename = "unblind_failed")]
UnblindFailed { reason: String, status: u16 },
#[serde(rename = "storage_failed")]
StorageFailed { reason: String, status: u16 },
}
impl CloseError {
pub fn status_code(&self) -> u16 {
match self {
Self::ValidationFailed { status, .. }
| Self::UnknownChannel { status }
| Self::AlreadyClosed { status, .. }
| Self::MintRejected { status, .. }
| Self::MintRejectedAfterRetry { status, .. }
| Self::UnblindFailed { status, .. }
| Self::StorageFailed { status, .. } => *status,
}
}
pub fn from_preparation_error(err: ClosePreparationError) -> Self {
let (expected_balance, actual_balance) = if let Some(extra) = &err.extra {
(
extra.get("expected").and_then(|v| v.as_u64()),
extra.get("actual").and_then(|v| v.as_u64()),
)
} else {
(None, None)
};
Self::ValidationFailed {
reason: err.reason,
status: err.status,
expected_balance,
actual_balance,
}
}
pub fn unknown_channel() -> Self {
Self::UnknownChannel { status: 404 }
}
pub fn mint_rejected(mint_error: serde_json::Value) -> Self {
Self::MintRejected {
mint_error,
status: 502,
}
}
pub fn mint_rejected_after_retry(
original_error: serde_json::Value,
retry_error: serde_json::Value,
) -> Self {
Self::MintRejectedAfterRetry {
original_error,
retry_error,
status: 502,
}
}
pub fn unblind_failed(reason: impl Into<String>) -> Self {
Self::UnblindFailed {
reason: reason.into(),
status: 500,
}
}
pub fn storage_failed(reason: impl Into<String>) -> Self {
Self::StorageFailed {
reason: reason.into(),
status: 500,
}
}
}
fn parse_mint_error_value(raw: &str) -> serde_json::Value {
serde_json::from_str(raw).unwrap_or_else(|_| serde_json::Value::String(raw.to_string()))
}
fn extract_nut00_error_code(raw: &str) -> Option<u32> {
serde_json::from_str::<serde_json::Value>(raw)
.ok()
.and_then(|v| v.get("code")?.as_u64())
.map(|c| c as u32)
}
fn is_keyset_error_code(code: u32) -> bool {
(12000..13000).contains(&code) || code == 99999
}
fn should_retry_swap_error(raw: &str) -> bool {
match extract_nut00_error_code(raw) {
Some(code) => {
let retryable = is_keyset_error_code(code);
if retryable {
tracing::debug!(
code,
"Keyset error detected, will retry after refreshing keysets"
);
} else {
tracing::debug!(code, "Non-retryable NUT-00 error code, failing immediately");
}
retryable
}
None => {
tracing::debug!(error = %raw, "Could not parse NUT-00 error code, failing immediately");
false
}
}
}
impl std::fmt::Display for CloseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ValidationFailed { reason, .. } => write!(f, "validation failed: {}", reason),
Self::UnknownChannel { .. } => write!(f, "unknown channel"),
Self::AlreadyClosed {
closed_balance,
requested_balance,
..
} => write!(
f,
"channel already closed with balance {} (requested {})",
closed_balance, requested_balance
),
Self::MintRejected { mint_error, .. } => {
write!(f, "mint rejected swap: {}", mint_error)
}
Self::MintRejectedAfterRetry {
original_error,
retry_error,
..
} => write!(
f,
"mint rejected swap after retry: original={}, retry={}",
original_error, retry_error
),
Self::UnblindFailed { reason, .. } => write!(f, "unblind failed: {}", reason),
Self::StorageFailed { reason, .. } => write!(f, "storage failed: {}", reason),
}
}
}
impl std::error::Error for CloseError {}
#[derive(Debug, Clone)]
pub struct ChannelPolicy {
pub min_expiry_in_seconds: u64,
pub min_capacity: u64,
pub max_amount_per_output: Option<u64>,
}
#[derive(Debug)]
pub enum BridgeError {
InvalidRequest(String),
ChannelClosed,
ChannelClosing,
ServerMisconfigured(String),
CapacityTooSmall {
capacity: u64,
min_capacity: u64,
},
ExpiryTooSoon {
expiry_timestamp: u64,
min_expiry: u64,
now: u64,
},
MaxAmountExceeded {
amount: u64,
max_allowed: u64,
},
BalanceExceedsCapacity {
balance: u64,
capacity: u64,
},
UnsupportedUnit(String),
ChannelIdMismatch,
ValidationFailed(String),
UnknownChannel,
InvalidSignature(String),
InsufficientBalance {
balance: u64,
amount_due: u64,
},
BalanceMismatch {
expected: u64,
actual: u64,
},
Internal(String),
ReceiverKeyNotAcceptable,
MintOrKeysetNotAcceptable,
}
impl std::fmt::Display for BridgeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidRequest(s) => write!(f, "{}", s),
Self::ChannelClosed => write!(f, "channel closed"),
Self::ChannelClosing => write!(f, "channel closing, swap pending"),
Self::ServerMisconfigured(s) => write!(f, "server misconfigured: {}", s),
Self::CapacityTooSmall {
capacity,
min_capacity,
} => write!(f, "capacity too small: {} < {}", capacity, min_capacity),
Self::ExpiryTooSoon {
expiry_timestamp,
min_expiry,
now,
} => write!(
f,
"expiry too soon: {} < {} ({}s remaining)",
expiry_timestamp,
min_expiry,
expiry_timestamp.saturating_sub(*now)
),
Self::MaxAmountExceeded {
amount,
max_allowed,
} => write!(
f,
"max_amount_per_output exceeded: {} > {}",
amount, max_allowed
),
Self::BalanceExceedsCapacity { balance, capacity } => {
write!(f, "balance exceeds capacity: {} > {}", balance, capacity)
}
Self::UnsupportedUnit(u) => write!(f, "unsupported unit: {}", u),
Self::ChannelIdMismatch => write!(f, "channel_id mismatch"),
Self::ValidationFailed(s) => write!(f, "channel validation failed: {}", s),
Self::UnknownChannel => write!(f, "unknown channel"),
Self::InvalidSignature(s) => write!(f, "invalid signature: {}", s),
Self::InsufficientBalance {
balance,
amount_due,
} => write!(f, "insufficient balance: {} < {}", balance, amount_due),
Self::BalanceMismatch { expected, actual } => {
write!(f, "balance mismatch: expected {}, got {}", expected, actual)
}
Self::Internal(s) => write!(f, "internal error: {}", s),
Self::ReceiverKeyNotAcceptable => write!(f, "receiver key not acceptable"),
Self::MintOrKeysetNotAcceptable => write!(f, "mint or keyset not acceptable"),
}
}
}
impl BridgeError {
pub fn to_response(&self) -> BridgeErrorResponse {
BridgeErrorResponse::from_bridge_error(self)
}
pub fn to_response_json(&self) -> String {
self.to_response().to_json()
}
}
pub fn unblind_and_verify_stage1_response(
blind_signatures: Vec<BlindSignature>,
secrets_with_blinding: Vec<(DeterministicSecretWithBlinding, bool)>,
params: &ChannelParameters,
output_keyset_info: &KeysetInfo,
balance: u64,
) -> Result<UnblindResult, BridgeError> {
if blind_signatures.len() != secrets_with_blinding.len() {
return Err(BridgeError::Internal(
"Length mismatch between signatures and secrets".into(),
));
}
let mut secrets = Vec::with_capacity(secrets_with_blinding.len());
let mut blinding_factors = Vec::with_capacity(secrets_with_blinding.len());
let mut is_receiver_flags = Vec::with_capacity(secrets_with_blinding.len());
let mut amount_index_pairs = Vec::with_capacity(secrets_with_blinding.len());
for (swb, is_receiver) in secrets_with_blinding {
secrets.push(swb.secret);
blinding_factors.push(swb.blinding_factor);
is_receiver_flags.push(is_receiver);
amount_index_pairs.push((swb.amount, swb.index));
}
let proofs = cashu::dhke::construct_proofs(
blind_signatures,
blinding_factors,
secrets,
&output_keyset_info.active_keys,
)
.map_err(|e| BridgeError::Internal(format!("Failed to construct proofs: {}", e)))?;
for (i, proof) in proofs.iter().enumerate() {
let mint_pubkey = output_keyset_info
.active_keys
.amount_key(proof.amount)
.ok_or_else(|| BridgeError::Internal("Missing mint key".into()))?;
proof.verify_dleq(mint_pubkey).map_err(|e| {
BridgeError::ValidationFailed(format!("DLEQ failed for proof {}: {}", i, e))
})?;
}
let mut receiver_proofs = Vec::new();
let mut sender_proofs = Vec::new();
let mut receiver_sum = 0;
let mut sender_sum = 0;
for ((mut proof, is_receiver), (amount, index)) in proofs
.into_iter()
.zip(is_receiver_flags)
.zip(amount_index_pairs)
{
let role = if is_receiver {
Stage2Role::Receiver
} else {
Stage2Role::Sender
};
params
.attach_stage2_p2pk_e(&mut proof, role, amount, index)
.map_err(|e| BridgeError::Internal(e.to_string()))?;
if is_receiver {
let expected_pubkey = params
.get_receiver_blinded_pubkey_for_stage2_output(amount, index)
.map_err(|e| BridgeError::Internal(e.to_string()))?;
let secret_json: serde_json::Value = serde_json::from_str(&proof.secret.to_string())
.map_err(|e| BridgeError::Internal(e.to_string()))?;
if secret_json.get(0).and_then(|v| v.as_str()) != Some("P2PK")
|| secret_json
.get(1)
.and_then(|v| v.get("data"))
.and_then(|v| v.as_str())
!= Some(&expected_pubkey.to_hex())
{
return Err(BridgeError::ValidationFailed(
"Receiver proof locked to wrong pubkey".into(),
));
}
receiver_sum += u64::from(proof.amount);
receiver_proofs.push(ProofWithMeta {
proof,
amount,
index,
is_receiver: true,
});
} else {
sender_sum += u64::from(proof.amount);
sender_proofs.push(ProofWithMeta {
proof,
amount,
index,
is_receiver: false,
});
}
}
let expected_nominal = output_keyset_info
.inverse_deterministic_value_after_fees(balance, params.maximum_amount_for_one_output)
.map_err(|e| BridgeError::Internal(e.to_string()))?
.nominal_value;
if receiver_sum != expected_nominal {
return Err(BridgeError::ValidationFailed(format!(
"Receiver nominal mismatch: expected {}, got {}",
expected_nominal, receiver_sum
)));
}
Ok(UnblindResult {
receiver_proofs,
sender_proofs,
receiver_sum,
sender_sum,
})
}
mod close;
mod payment;
#[cfg(test)]
mod tests;
#[cfg(test)]
#[path = "bridge_persistence_tests.rs"]
mod persistence_tests;