mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-21 14:44:46 +01:00
The main goal is to add a subscription to CDK Mint updates into the wallet. This feature will be particularly useful for improving the code whenever loops hit the mint server to check status changes. The goal is to add an easy-to-use interface that will hide the fact that we're connecting to WebSocket and subscribing to events. This will also hide the fact that the CDK-mint server may not support WebSocket updates. To be fully backward compatible, the HttpClientMethods traits have a new method, `subscribe,` which will return an object that implements `ActiveSubscription.` In the primary implementation, there is a `SubscriptionClient` that will attempt to connect through WebSocket and will fall to the HTTP-status pull and sleep approach (the current approach), but upper stream code will receive updates as if they come from a stream of updates through WebSocket. This `SubscriptionClient` struct will also manage reconnections to WebSockets (with automatic resubscriptions) and all the low-level stuff, providing an easy-to-use interface and leaving the upper-level code with a nice interface that is hard to misuse. When `ActiveSubscription` is dropped, it will automatically unsubscribe. Fixed bug with Default as described in https://github.com/cashubtc/cdk/pull/473#discussion_r1871032297
435 lines
13 KiB
Rust
435 lines
13 KiB
Rust
//! Mint tests
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::{bail, Result};
|
|
use bip39::Mnemonic;
|
|
use cdk::amount::{Amount, SplitTarget};
|
|
use cdk::cdk_database::mint_memory::MintMemoryDatabase;
|
|
use cdk::dhke::construct_proofs;
|
|
use cdk::mint::MintQuote;
|
|
use cdk::nuts::nut00::ProofsMethods;
|
|
use cdk::nuts::nut17::Params;
|
|
use cdk::nuts::{
|
|
CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PreMintSecrets,
|
|
ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest,
|
|
};
|
|
use cdk::types::QuoteTTL;
|
|
use cdk::util::unix_time;
|
|
use cdk::Mint;
|
|
use tokio::sync::OnceCell;
|
|
use tokio::time::sleep;
|
|
|
|
pub const MINT_URL: &str = "http://127.0.0.1:8088";
|
|
|
|
static INSTANCE: OnceCell<Mint> = OnceCell::const_new();
|
|
|
|
async fn new_mint(fee: u64) -> Mint {
|
|
let mut supported_units = HashMap::new();
|
|
supported_units.insert(CurrencyUnit::Sat, (fee, 32));
|
|
|
|
let nuts = Nuts::new()
|
|
.nut07(true)
|
|
.nut08(true)
|
|
.nut09(true)
|
|
.nut10(true)
|
|
.nut11(true)
|
|
.nut12(true)
|
|
.nut14(true);
|
|
|
|
let mint_info = MintInfo::new().nuts(nuts);
|
|
|
|
let mnemonic = Mnemonic::generate(12).unwrap();
|
|
|
|
let quote_ttl = QuoteTTL::new(10000, 10000);
|
|
|
|
Mint::new(
|
|
MINT_URL,
|
|
&mnemonic.to_seed_normalized(""),
|
|
mint_info,
|
|
quote_ttl,
|
|
Arc::new(MintMemoryDatabase::default()),
|
|
HashMap::new(),
|
|
supported_units,
|
|
HashMap::new(),
|
|
)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
async fn initialize() -> &'static Mint {
|
|
INSTANCE.get_or_init(|| new_mint(0)).await
|
|
}
|
|
|
|
async fn mint_proofs(
|
|
mint: &Mint,
|
|
amount: Amount,
|
|
split_target: &SplitTarget,
|
|
keys: cdk::nuts::Keys,
|
|
) -> Result<Proofs> {
|
|
let request_lookup = uuid::Uuid::new_v4().to_string();
|
|
|
|
let quote = MintQuote::new(
|
|
mint.mint_url.clone(),
|
|
"".to_string(),
|
|
CurrencyUnit::Sat,
|
|
amount,
|
|
unix_time() + 36000,
|
|
request_lookup.to_string(),
|
|
);
|
|
|
|
mint.localstore.add_mint_quote(quote.clone()).await?;
|
|
|
|
mint.pay_mint_quote_for_request_id(&request_lookup).await?;
|
|
let keyset_id = Id::from(&keys);
|
|
|
|
let premint = PreMintSecrets::random(keyset_id, amount, split_target)?;
|
|
|
|
let mint_request = MintBolt11Request {
|
|
quote: quote.id,
|
|
outputs: premint.blinded_messages(),
|
|
};
|
|
|
|
let after_mint = mint.process_mint_request(mint_request).await?;
|
|
|
|
let proofs = construct_proofs(
|
|
after_mint.signatures,
|
|
premint.rs(),
|
|
premint.secrets(),
|
|
&keys,
|
|
)?;
|
|
|
|
Ok(proofs)
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
async fn test_mint_double_spend() -> Result<()> {
|
|
let mint = initialize().await;
|
|
|
|
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
|
|
let keyset_id = Id::from(&keys);
|
|
|
|
let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?;
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
|
|
|
|
let swap = mint.process_swap_request(swap_request).await;
|
|
|
|
assert!(swap.is_ok());
|
|
|
|
let preswap_two = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?;
|
|
|
|
let swap_two_request = SwapRequest::new(proofs, preswap_two.blinded_messages());
|
|
|
|
match mint.process_swap_request(swap_two_request).await {
|
|
Ok(_) => bail!("Proofs double spent"),
|
|
Err(err) => match err {
|
|
cdk::Error::TokenAlreadySpent => (),
|
|
_ => bail!("Wrong error returned"),
|
|
},
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// This attempts to swap for more outputs then inputs.
|
|
/// This will work if the mint does not check for outputs amounts overflowing
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
async fn test_attempt_to_swap_by_overflowing() -> Result<()> {
|
|
let mint = initialize().await;
|
|
|
|
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
|
|
let keyset_id = Id::from(&keys);
|
|
|
|
let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?;
|
|
|
|
let amount = 2_u64.pow(63);
|
|
|
|
let pre_mint_amount =
|
|
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default())?;
|
|
let pre_mint_amount_two =
|
|
PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default())?;
|
|
|
|
let mut pre_mint = PreMintSecrets::random(keyset_id, 1.into(), &SplitTarget::default())?;
|
|
|
|
pre_mint.combine(pre_mint_amount);
|
|
pre_mint.combine(pre_mint_amount_two);
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
|
|
|
|
match mint.process_swap_request(swap_request).await {
|
|
Ok(_) => bail!("Swap occurred with overflow"),
|
|
Err(err) => match err {
|
|
cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)) => (),
|
|
_ => {
|
|
println!("{:?}", err);
|
|
bail!("Wrong error returned in swap overflow")
|
|
}
|
|
},
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
pub async fn test_p2pk_swap() -> Result<()> {
|
|
let mint = initialize().await;
|
|
|
|
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
|
|
let keyset_id = Id::from(&keys);
|
|
|
|
let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?;
|
|
|
|
let secret = SecretKey::generate();
|
|
|
|
let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
|
|
|
|
let pre_swap = PreMintSecrets::with_conditions(
|
|
keyset_id,
|
|
100.into(),
|
|
&SplitTarget::default(),
|
|
&spending_conditions,
|
|
)?;
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
|
|
|
|
let keys = mint.pubkeys().await?.keysets.first().cloned().unwrap().keys;
|
|
|
|
let post_swap = mint.process_swap_request(swap_request).await?;
|
|
|
|
let mut proofs = construct_proofs(
|
|
post_swap.signatures,
|
|
pre_swap.rs(),
|
|
pre_swap.secrets(),
|
|
&keys,
|
|
)?;
|
|
|
|
let pre_swap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
|
|
|
|
let public_keys_to_listen: Vec<_> = swap_request
|
|
.inputs
|
|
.ys()
|
|
.expect("key")
|
|
.into_iter()
|
|
.enumerate()
|
|
.filter_map(|(key, pk)| {
|
|
if key % 2 == 0 {
|
|
// Only expect messages from every other key
|
|
Some(pk.to_string())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let mut listener = mint
|
|
.pubsub_manager
|
|
.try_subscribe(Params {
|
|
kind: cdk::nuts::nut17::Kind::ProofState,
|
|
filters: public_keys_to_listen.clone(),
|
|
id: "test".into(),
|
|
})
|
|
.await
|
|
.expect("valid subscription");
|
|
|
|
match mint.process_swap_request(swap_request).await {
|
|
Ok(_) => bail!("Proofs spent without sig"),
|
|
Err(err) => match err {
|
|
cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided) => (),
|
|
_ => {
|
|
println!("{:?}", err);
|
|
bail!("Wrong error returned")
|
|
}
|
|
},
|
|
}
|
|
|
|
for proof in &mut proofs {
|
|
proof.sign_p2pk(secret.clone())?;
|
|
}
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
|
|
|
|
let attempt_swap = mint.process_swap_request(swap_request).await;
|
|
|
|
assert!(attempt_swap.is_ok());
|
|
|
|
sleep(Duration::from_millis(10)).await;
|
|
|
|
let mut msgs = HashMap::new();
|
|
while let Ok((sub_id, msg)) = listener.try_recv() {
|
|
assert_eq!(sub_id, "test".into());
|
|
match msg {
|
|
NotificationPayload::ProofState(ProofState { y, state, .. }) => {
|
|
let pk = y.to_string();
|
|
msgs.get_mut(&pk)
|
|
.map(|x: &mut Vec<State>| {
|
|
x.push(state);
|
|
})
|
|
.unwrap_or_else(|| {
|
|
msgs.insert(pk, vec![state]);
|
|
});
|
|
}
|
|
_ => bail!("Wrong message received"),
|
|
}
|
|
}
|
|
|
|
for keys in public_keys_to_listen {
|
|
let statuses = msgs.remove(&keys).expect("some events");
|
|
assert_eq!(statuses, vec![State::Pending, State::Pending, State::Spent]);
|
|
}
|
|
|
|
assert!(listener.try_recv().is_err(), "no other event is happening");
|
|
assert!(msgs.is_empty(), "Only expected key events are received");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
async fn test_swap_unbalanced() -> Result<()> {
|
|
let mint = initialize().await;
|
|
|
|
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
|
|
let keyset_id = Id::from(&keys);
|
|
|
|
let proofs = mint_proofs(mint, 100.into(), &SplitTarget::default(), keys).await?;
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 95.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
|
|
|
|
match mint.process_swap_request(swap_request).await {
|
|
Ok(_) => bail!("Swap was allowed unbalanced"),
|
|
Err(err) => match err {
|
|
cdk::Error::TransactionUnbalanced(_, _, _) => (),
|
|
_ => bail!("Wrong error returned"),
|
|
},
|
|
}
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 101.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
|
|
|
|
match mint.process_swap_request(swap_request).await {
|
|
Ok(_) => bail!("Swap was allowed unbalanced"),
|
|
Err(err) => match err {
|
|
cdk::Error::TransactionUnbalanced(_, _, _) => (),
|
|
_ => bail!("Wrong error returned"),
|
|
},
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
async fn test_swap_overpay_underpay_fee() -> Result<()> {
|
|
let mint = new_mint(1).await;
|
|
|
|
mint.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, HashMap::new())
|
|
.await?;
|
|
|
|
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
|
|
let keyset_id = Id::from(&keys);
|
|
|
|
let proofs = mint_proofs(&mint, 1000.into(), &SplitTarget::default(), keys).await?;
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
|
|
|
|
// Attempt to swap overpaying fee
|
|
match mint.process_swap_request(swap_request).await {
|
|
Ok(_) => bail!("Swap was allowed unbalanced"),
|
|
Err(err) => match err {
|
|
cdk::Error::TransactionUnbalanced(_, _, _) => (),
|
|
_ => {
|
|
println!("{:?}", err);
|
|
bail!("Wrong error returned")
|
|
}
|
|
},
|
|
}
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
|
|
|
|
// Attempt to swap underpaying fee
|
|
match mint.process_swap_request(swap_request).await {
|
|
Ok(_) => bail!("Swap was allowed unbalanced"),
|
|
Err(err) => match err {
|
|
cdk::Error::TransactionUnbalanced(_, _, _) => (),
|
|
_ => {
|
|
println!("{:?}", err);
|
|
bail!("Wrong error returned")
|
|
}
|
|
},
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
async fn test_mint_enforce_fee() -> Result<()> {
|
|
let mint = new_mint(1).await;
|
|
|
|
let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys;
|
|
let keyset_id = Id::from(&keys);
|
|
|
|
let mut proofs = mint_proofs(&mint, 1010.into(), &SplitTarget::Value(1.into()), keys).await?;
|
|
|
|
let five_proofs: Vec<_> = proofs.drain(..5).collect();
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 5.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
|
|
|
|
// Attempt to swap underpaying fee
|
|
match mint.process_swap_request(swap_request).await {
|
|
Ok(_) => bail!("Swap was allowed unbalanced"),
|
|
Err(err) => match err {
|
|
cdk::Error::TransactionUnbalanced(_, _, _) => (),
|
|
_ => {
|
|
println!("{:?}", err);
|
|
bail!("Wrong error returned")
|
|
}
|
|
},
|
|
}
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 4.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
|
|
|
|
let _ = mint.process_swap_request(swap_request).await?;
|
|
|
|
let thousnad_proofs: Vec<_> = proofs.drain(..1001).collect();
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
|
|
|
|
// Attempt to swap underpaying fee
|
|
match mint.process_swap_request(swap_request).await {
|
|
Ok(_) => bail!("Swap was allowed unbalanced"),
|
|
Err(err) => match err {
|
|
cdk::Error::TransactionUnbalanced(_, _, _) => (),
|
|
_ => {
|
|
println!("{:?}", err);
|
|
bail!("Wrong error returned")
|
|
}
|
|
},
|
|
}
|
|
|
|
let preswap = PreMintSecrets::random(keyset_id, 999.into(), &SplitTarget::default())?;
|
|
|
|
let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
|
|
|
|
let _ = mint.process_swap_request(swap_request).await?;
|
|
|
|
Ok(())
|
|
}
|