use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use async_trait::async_trait;
use bitcoin::bip32::DerivationPath;
use bitcoin::hashes::{sha256, Hash, HashEngine};
use cashu::amount::SplitTarget;
use cashu::nuts::nut07::ProofState;
use cashu::nuts::nut18::PaymentRequest;
use cashu::nuts::AuthProof;
use cashu::util::hex;
use cashu::{nut00, PaymentMethod, Proof, Proofs, PublicKey};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::mint_url::MintUrl;
use crate::nuts::{
CurrencyUnit, Id, MeltQuoteState, MintQuoteState, SecretKey, SpendingConditions, State,
};
use crate::{Amount, Error};
pub mod saga;
pub use saga::{
IssueSagaState, MeltOperationData, MeltSagaState, MintOperationData, OperationData,
ReceiveOperationData, ReceiveSagaState, SendOperationData, SendSagaState, SwapOperationData,
SwapSagaState, WalletSaga, WalletSagaState,
};
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct WalletKey {
pub mint_url: MintUrl,
pub unit: CurrencyUnit,
}
impl fmt::Display for WalletKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
}
}
impl WalletKey {
pub fn new(mint_url: MintUrl, unit: CurrencyUnit) -> Self {
Self { mint_url, unit }
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofInfo {
pub proof: Proof,
pub y: PublicKey,
pub mint_url: MintUrl,
pub state: State,
pub spending_condition: Option<SpendingConditions>,
pub unit: CurrencyUnit,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub used_by_operation: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_by_operation: Option<Uuid>,
}
impl ProofInfo {
pub fn new(
proof: Proof,
mint_url: MintUrl,
state: State,
unit: CurrencyUnit,
) -> Result<Self, Error> {
let y = proof.y()?;
let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
Ok(Self {
proof,
y,
mint_url,
state,
spending_condition,
unit,
used_by_operation: None,
created_by_operation: None,
})
}
pub fn new_with_operations(
proof: Proof,
mint_url: MintUrl,
state: State,
unit: CurrencyUnit,
used_by_operation: Option<Uuid>,
created_by_operation: Option<Uuid>,
) -> Result<Self, Error> {
let y = proof.y()?;
let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
Ok(Self {
proof,
y,
mint_url,
state,
spending_condition,
unit,
used_by_operation,
created_by_operation,
})
}
pub fn matches_conditions(
&self,
mint_url: &Option<MintUrl>,
unit: &Option<CurrencyUnit>,
state: &Option<Vec<State>>,
spending_conditions: &Option<Vec<SpendingConditions>>,
) -> bool {
if let Some(mint_url) = mint_url {
if mint_url.ne(&self.mint_url) {
return false;
}
}
if let Some(unit) = unit {
if unit.ne(&self.unit) {
return false;
}
}
if let Some(state) = state {
if !state.contains(&self.state) {
return false;
}
}
if let Some(spending_conditions) = spending_conditions {
match &self.spending_condition {
None => {
if !spending_conditions.is_empty() {
return false;
}
}
Some(s) => {
if !spending_conditions.contains(s) {
return false;
}
}
}
}
true
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintQuote {
pub id: String,
pub mint_url: MintUrl,
pub payment_method: PaymentMethod,
pub amount: Option<Amount>,
pub unit: CurrencyUnit,
pub request: String,
pub state: MintQuoteState,
pub expiry: u64,
pub secret_key: Option<SecretKey>,
#[serde(default)]
pub amount_issued: Amount,
#[serde(default)]
pub amount_paid: Amount,
#[serde(default)]
pub used_by_operation: Option<String>,
#[serde(default)]
pub version: u32,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct MeltQuote {
pub id: String,
pub mint_url: Option<MintUrl>,
pub unit: CurrencyUnit,
pub amount: Amount,
pub request: String,
pub fee_reserve: Amount,
pub state: MeltQuoteState,
pub expiry: u64,
pub payment_preimage: Option<String>,
pub payment_method: PaymentMethod,
#[serde(default)]
pub used_by_operation: Option<String>,
#[serde(default)]
pub version: u32,
}
impl MintQuote {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
mint_url: MintUrl,
payment_method: PaymentMethod,
amount: Option<Amount>,
unit: CurrencyUnit,
request: String,
expiry: u64,
secret_key: Option<SecretKey>,
) -> Self {
Self {
id,
mint_url,
payment_method,
amount,
unit,
request,
state: MintQuoteState::Unpaid,
expiry,
secret_key,
amount_issued: Amount::ZERO,
amount_paid: Amount::ZERO,
used_by_operation: None,
version: 0,
}
}
pub fn total_amount(&self) -> Amount {
self.amount_paid
}
pub fn is_expired(&self, current_time: u64) -> bool {
current_time > self.expiry
}
pub fn amount_mintable(&self) -> Amount {
if self.payment_method == PaymentMethod::BOLT11 {
if self.state == MintQuoteState::Paid {
self.amount.unwrap_or(Amount::ZERO)
} else {
Amount::ZERO
}
} else {
self.amount_paid
.checked_sub(self.amount_issued)
.unwrap_or(Amount::ZERO)
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
pub struct Restored {
pub spent: Amount,
pub unspent: Amount,
pub pending: Amount,
}
#[derive(Debug, Clone, Default)]
pub struct SendOptions {
pub memo: Option<SendMemo>,
pub conditions: Option<SpendingConditions>,
pub amount_split_target: SplitTarget,
pub send_kind: SendKind,
pub include_fee: bool,
pub max_proofs: Option<usize>,
pub metadata: HashMap<String, String>,
pub use_p2bk: bool,
}
#[derive(Debug, Clone)]
pub struct SendMemo {
pub memo: String,
pub include_memo: bool,
}
impl SendMemo {
pub fn for_token(memo: &str) -> Self {
Self {
memo: memo.to_string(),
include_memo: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ReceiveOptions {
pub amount_split_target: SplitTarget,
pub p2pk_signing_keys: Vec<SecretKey>,
pub preimages: Vec<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum SendKind {
#[default]
OnlineExact,
OnlineTolerance(Amount),
OfflineExact,
OfflineTolerance(Amount),
}
impl SendKind {
pub fn is_online(&self) -> bool {
matches!(self, Self::OnlineExact | Self::OnlineTolerance(_))
}
pub fn is_offline(&self) -> bool {
matches!(self, Self::OfflineExact | Self::OfflineTolerance(_))
}
pub fn is_exact(&self) -> bool {
matches!(self, Self::OnlineExact | Self::OfflineExact)
}
pub fn has_tolerance(&self) -> bool {
matches!(self, Self::OnlineTolerance(_) | Self::OfflineTolerance(_))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Transaction {
pub mint_url: MintUrl,
pub direction: TransactionDirection,
pub amount: Amount,
pub fee: Amount,
pub unit: CurrencyUnit,
pub ys: Vec<PublicKey>,
pub timestamp: u64,
pub memo: Option<String>,
pub metadata: HashMap<String, String>,
pub quote_id: Option<String>,
pub payment_request: Option<String>,
pub payment_proof: Option<String>,
#[serde(default)]
pub payment_method: Option<PaymentMethod>,
#[serde(default)]
pub saga_id: Option<Uuid>,
}
impl Transaction {
pub fn id(&self) -> TransactionId {
TransactionId::new(self.ys.clone())
}
pub fn matches_conditions(
&self,
mint_url: &Option<MintUrl>,
direction: &Option<TransactionDirection>,
unit: &Option<CurrencyUnit>,
) -> bool {
if let Some(mint_url) = mint_url {
if &self.mint_url != mint_url {
return false;
}
}
if let Some(direction) = direction {
if &self.direction != direction {
return false;
}
}
if let Some(unit) = unit {
if &self.unit != unit {
return false;
}
}
true
}
}
impl PartialOrd for Transaction {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Transaction {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.timestamp
.cmp(&other.timestamp)
.reverse()
.then_with(|| self.id().cmp(&other.id()))
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransactionDirection {
Incoming,
Outgoing,
}
impl std::fmt::Display for TransactionDirection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransactionDirection::Incoming => write!(f, "Incoming"),
TransactionDirection::Outgoing => write!(f, "Outgoing"),
}
}
}
impl FromStr for TransactionDirection {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"Incoming" => Ok(Self::Incoming),
"Outgoing" => Ok(Self::Outgoing),
_ => Err(Error::InvalidTransactionDirection),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TransactionId([u8; 32]);
impl TransactionId {
pub fn new(ys: Vec<PublicKey>) -> Self {
let mut ys = ys;
ys.sort();
let mut hasher = sha256::Hash::engine();
for y in ys {
hasher.input(&y.to_bytes());
}
let hash = sha256::Hash::from_engine(hasher);
Self(hash.to_byte_array())
}
pub fn from_proofs(proofs: Proofs) -> Result<Self, nut00::Error> {
let ys = proofs
.iter()
.map(|proof| proof.y())
.collect::<Result<Vec<PublicKey>, nut00::Error>>()?;
Ok(Self::new(ys))
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn from_hex(value: &str) -> Result<Self, Error> {
let bytes = hex::decode(value)?;
if bytes.len() != 32 {
return Err(Error::InvalidTransactionId);
}
let mut array = [0u8; 32];
array.copy_from_slice(&bytes);
Ok(Self(array))
}
pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
if slice.len() != 32 {
return Err(Error::InvalidTransactionId);
}
let mut array = [0u8; 32];
array.copy_from_slice(slice);
Ok(Self(array))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn as_slice(&self) -> &[u8] {
&self.0
}
}
impl std::fmt::Display for TransactionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
impl FromStr for TransactionId {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::from_hex(value)
}
}
impl TryFrom<Proofs> for TransactionId {
type Error = nut00::Error;
fn try_from(proofs: Proofs) -> Result<Self, Self::Error> {
Self::from_proofs(proofs)
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationKind {
Send,
Receive,
Swap,
Mint,
Melt,
}
impl fmt::Display for OperationKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OperationKind::Send => write!(f, "send"),
OperationKind::Receive => write!(f, "receive"),
OperationKind::Swap => write!(f, "swap"),
OperationKind::Mint => write!(f, "mint"),
OperationKind::Melt => write!(f, "melt"),
}
}
}
impl FromStr for OperationKind {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"send" => Ok(OperationKind::Send),
"receive" => Ok(OperationKind::Receive),
"swap" => Ok(OperationKind::Swap),
"mint" => Ok(OperationKind::Mint),
"melt" => Ok(OperationKind::Melt),
_ => Err(Error::InvalidOperationKind),
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait Wallet: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static;
type Amount: Clone + Send + Sync;
type MintUrl: Clone + Send + Sync;
type CurrencyUnit: Clone + Send + Sync;
type MintInfo: Clone + Send + Sync;
type KeySetInfo: Clone + Send + Sync;
type MintQuote: Clone + Send + Sync;
type MeltQuote: Clone + Send + Sync;
type PaymentMethod: Clone + Send + Sync;
type MeltOptions: Clone + Send + Sync;
type OperationId: Clone + Send + Sync;
type PreparedSend<'a>: Send + Sync
where
Self: 'a;
type PreparedMelt<'a>: Send + Sync
where
Self: 'a;
type Subscription: Send + Sync;
type SubscribeParams: Clone + Send + Sync;
fn mint_url(&self) -> Self::MintUrl;
fn unit(&self) -> Self::CurrencyUnit;
async fn total_balance(&self) -> Result<Self::Amount, Self::Error>;
async fn total_pending_balance(&self) -> Result<Self::Amount, Self::Error>;
async fn total_reserved_balance(&self) -> Result<Self::Amount, Self::Error>;
async fn fetch_mint_info(&self) -> Result<Option<Self::MintInfo>, Self::Error>;
async fn load_mint_info(&self) -> Result<Self::MintInfo, Self::Error>;
async fn refresh_keysets(&self) -> Result<Vec<Self::KeySetInfo>, Self::Error>;
async fn get_active_keyset(&self) -> Result<Self::KeySetInfo, Self::Error>;
async fn mint_quote(
&self,
method: Self::PaymentMethod,
amount: Option<Self::Amount>,
description: Option<String>,
extra: Option<String>,
) -> Result<Self::MintQuote, Self::Error>;
async fn melt_quote(
&self,
method: Self::PaymentMethod,
request: String,
options: Option<Self::MeltOptions>,
extra: Option<String>,
) -> Result<Self::MeltQuote, Self::Error>;
async fn list_transactions(
&self,
direction: Option<TransactionDirection>,
) -> Result<Vec<Transaction>, Self::Error>;
async fn get_transaction(&self, id: TransactionId) -> Result<Option<Transaction>, Self::Error>;
async fn get_proofs_for_transaction(&self, id: TransactionId) -> Result<Proofs, Self::Error>;
async fn revert_transaction(&self, id: TransactionId) -> Result<(), Self::Error>;
async fn check_all_pending_proofs(&self) -> Result<Self::Amount, Self::Error>;
async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Self::Error>;
async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Self::Error>;
async fn calculate_fee(
&self,
proof_count: u64,
keyset_id: Id,
) -> Result<Self::Amount, Self::Error>;
async fn receive(
&self,
encoded_token: &str,
options: ReceiveOptions,
) -> Result<Self::Amount, Self::Error>;
async fn receive_proofs(
&self,
proofs: Proofs,
options: ReceiveOptions,
memo: Option<String>,
token: Option<String>,
) -> Result<Self::Amount, Self::Error>;
async fn prepare_send(
&self,
amount: Self::Amount,
options: SendOptions,
) -> Result<Self::PreparedSend<'_>, Self::Error>;
async fn get_pending_sends(&self) -> Result<Vec<Self::OperationId>, Self::Error>;
async fn revoke_send(
&self,
operation_id: Self::OperationId,
) -> Result<Self::Amount, Self::Error>;
async fn check_send_status(&self, operation_id: Self::OperationId)
-> Result<bool, Self::Error>;
async fn mint(
&self,
quote_id: &str,
split_target: SplitTarget,
spending_conditions: Option<SpendingConditions>,
) -> Result<Proofs, Self::Error>;
async fn check_mint_quote_status(&self, quote_id: &str)
-> Result<Self::MintQuote, Self::Error>;
async fn fetch_mint_quote(
&self,
quote_id: &str,
payment_method: Option<Self::PaymentMethod>,
) -> Result<Self::MintQuote, Self::Error>;
async fn prepare_melt(
&self,
quote_id: &str,
metadata: HashMap<String, String>,
) -> Result<Self::PreparedMelt<'_>, Self::Error>;
async fn prepare_melt_proofs(
&self,
quote_id: &str,
proofs: Proofs,
metadata: HashMap<String, String>,
) -> Result<Self::PreparedMelt<'_>, Self::Error>;
async fn swap(
&self,
amount: Option<Self::Amount>,
split_target: SplitTarget,
input_proofs: Proofs,
spending_conditions: Option<SpendingConditions>,
include_fees: bool,
use_p2bk: bool,
) -> Result<Option<Proofs>, Self::Error>;
async fn set_cat(&self, cat: String) -> Result<(), Self::Error>;
async fn set_refresh_token(&self, refresh_token: String) -> Result<(), Self::Error>;
async fn refresh_access_token(&self) -> Result<(), Self::Error>;
async fn mint_blind_auth(&self, amount: Self::Amount) -> Result<Proofs, Self::Error>;
async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Self::Error>;
async fn restore(&self) -> Result<Restored, Self::Error>;
async fn verify_token_dleq(&self, token_str: &str) -> Result<(), Self::Error>;
async fn pay_request(
&self,
request: PaymentRequest,
custom_amount: Option<Self::Amount>,
) -> Result<(), Self::Error>;
async fn subscribe_mint_quote_state(
&self,
quote_ids: Vec<String>,
method: Self::PaymentMethod,
) -> Result<Self::Subscription, Self::Error>;
fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>);
async fn subscribe(
&self,
params: Self::SubscribeParams,
) -> Result<Self::Subscription, Self::Error>;
#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
async fn melt_bip353_quote(
&self,
bip353_address: &str,
amount_msat: Self::Amount,
network: bitcoin::Network,
) -> Result<Self::MeltQuote, Self::Error>;
#[cfg(not(target_arch = "wasm32"))]
async fn melt_lightning_address_quote(
&self,
lightning_address: &str,
amount_msat: Self::Amount,
) -> Result<Self::MeltQuote, Self::Error>;
#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
async fn melt_human_readable_quote(
&self,
address: &str,
amount_msat: Self::Amount,
network: bitcoin::Network,
) -> Result<Self::MeltQuote, Self::Error>;
#[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
async fn melt_human_readable(
&self,
address: &str,
amount_msat: Self::Amount,
network: bitcoin::Network,
) -> Result<Self::MeltQuote, Self::Error> {
self.melt_human_readable_quote(address, amount_msat, network)
.await
}
async fn check_mint_quote(&self, quote_id: &str) -> Result<Self::MintQuote, Self::Error> {
self.check_mint_quote_status(quote_id).await
}
async fn mint_unified(
&self,
quote_id: &str,
split_target: SplitTarget,
spending_conditions: Option<SpendingConditions>,
) -> Result<Proofs, Self::Error> {
self.mint(quote_id, split_target, spending_conditions).await
}
async fn get_proofs_by_states(&self, states: Vec<State>) -> Result<Proofs, Self::Error>;
async fn generate_public_key(&self) -> Result<PublicKey, Self::Error>;
async fn get_public_key(
&self,
pubkey: &PublicKey,
) -> Result<Option<P2PKSigningKey>, Self::Error>;
async fn get_public_keys(&self) -> Result<Vec<P2PKSigningKey>, Self::Error>;
async fn get_latest_public_key(&self) -> Result<Option<P2PKSigningKey>, Self::Error>;
async fn get_signing_key(&self, pubkey: &PublicKey) -> Result<Option<SecretKey>, Self::Error>;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct P2PKSigningKey {
pub pubkey: PublicKey,
pub derivation_path: DerivationPath,
pub derivation_index: u32,
pub created_time: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nuts::Id;
use crate::secret::Secret;
#[test]
fn test_transaction_id_from_hex() {
let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1c";
let transaction_id = TransactionId::from_hex(hex_str).unwrap();
assert_eq!(transaction_id.to_string(), hex_str);
}
#[test]
fn test_transaction_id_from_hex_empty_string() {
let hex_str = "";
let res = TransactionId::from_hex(hex_str);
assert!(matches!(res, Err(Error::InvalidTransactionId)));
}
#[test]
fn test_transaction_id_from_hex_longer_string() {
let hex_str = "a1b2c3d4e5f60718293a0b1c2d3e4f506172839a0b1c2d3e4f506172839a0b1ca1b2";
let res = TransactionId::from_hex(hex_str);
assert!(matches!(res, Err(Error::InvalidTransactionId)));
}
#[test]
fn test_matches_conditions() {
let keyset_id = Id::from_str("00deadbeef123456").unwrap();
let proof = Proof::new(
Amount::from(64),
keyset_id,
Secret::new("test_secret"),
PublicKey::from_hex(
"02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
)
.unwrap(),
);
let mint_url = MintUrl::from_str("https://example.com").unwrap();
let proof_info =
ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
assert!(!proof_info.matches_conditions(
&Some(MintUrl::from_str("https://different.com").unwrap()),
&None,
&None,
&None
));
assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
assert!(proof_info.matches_conditions(
&None,
&None,
&Some(vec![State::Unspent, State::Spent]),
&None
));
assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
assert!(proof_info.matches_conditions(&None, &None, &None, &None));
assert!(proof_info.matches_conditions(
&Some(mint_url),
&Some(CurrencyUnit::Sat),
&Some(vec![State::Unspent]),
&None
));
}
#[test]
fn test_matches_conditions_with_spending_conditions() {
let keyset_id = Id::from_str("00deadbeef123456").unwrap();
let proof = Proof::new(
Amount::from(64),
keyset_id,
Secret::new("test_secret"),
PublicKey::from_hex(
"02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
)
.unwrap(),
);
let mint_url = MintUrl::from_str("https://example.com").unwrap();
let proof_info =
ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
let dummy_condition = SpendingConditions::P2PKConditions {
data: SecretKey::generate().public_key(),
conditions: None,
};
assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
}
}