use std::sync::Arc;
use cdk_common::database::WalletDatabase;
use cdk_common::wallet::{
IssueSagaState, MintOperationData, OperationData, WalletSaga, WalletSagaState,
};
use super::*;
use crate::wallet::test_utils::{
create_test_db, create_test_wallet_with_mock, test_mint_quote, test_mint_url, MockMintConnector,
};
fn paid_response(quote: &MintQuote) -> MintQuoteResponse<String> {
MintQuoteResponse::Bolt11(cdk_common::MintQuoteBolt11Response {
quote: quote.id.clone(),
request: quote.request.clone(),
amount: quote.amount,
unit: Some(quote.unit.clone()),
method: quote.payment_method.clone(),
amount_paid: Amount::from(1_000),
amount_issued: Amount::ZERO,
updated_at: 1,
state: MintQuoteState::Paid,
expiry: Some(quote.expiry),
pubkey: None,
})
}
#[tokio::test]
async fn quote_status_preserves_recovery_ownership() {
for state in [
None,
Some(IssueSagaState::SecretsPrepared),
Some(IssueSagaState::MintRequested),
] {
let db = create_test_db().await;
let client = Arc::new(MockMintConnector::new());
let wallet = create_test_wallet_with_mock(db.clone(), client.clone()).await;
let quote = test_mint_quote(test_mint_url());
db.add_mint_quote(quote.clone()).await.unwrap();
let operation_id = uuid::Uuid::new_v4();
db.reserve_mint_quote("e.id, &operation_id)
.await
.unwrap();
let pending = state == Some(IssueSagaState::MintRequested);
if let Some(state) = state {
db.add_saga(WalletSaga::new(
operation_id,
WalletSagaState::Issue(state),
Amount::from(1_000),
quote.mint_url.clone(),
quote.unit.clone(),
OperationData::Mint(MintOperationData::new_single(
quote.id.clone(),
Amount::from(1_000),
None,
None,
None,
)),
))
.await
.unwrap();
}
client.set_mint_quote_status_response("e.id, paid_response("e));
for expected_version in 1..=2 {
let returned = wallet.check_mint_quote_status("e.id).await.unwrap();
let stored = db.get_mint_quote("e.id).await.unwrap().unwrap();
assert_eq!(returned, stored);
assert_eq!(returned.version, expected_version);
assert_eq!(returned.amount_paid, Amount::from(1_000));
assert_eq!(returned.amount_issued, Amount::ZERO);
assert_eq!(
returned.used_by_operation,
pending.then(|| operation_id.to_string())
);
assert_eq!(db.get_saga(&operation_id).await.unwrap().is_some(), pending);
}
assert!(db
.get_proofs(None, None, None, None)
.await
.unwrap()
.is_empty());
}
}
#[tokio::test]
async fn quote_status_returns_state_after_successful_recovery() {
use crate::amount::FeeAndAmounts;
use crate::nuts::PreMintSecrets;
let db = create_test_db().await;
let client = Arc::new(MockMintConnector::new());
client.enable_mint_signing();
let keyset_id = client.keysets.lock().unwrap()[0].id;
let wallet = create_test_wallet_with_mock(db.clone(), client.clone()).await;
let mut quote = test_mint_quote(test_mint_url());
quote.amount = Some(Amount::from(1));
quote.amount_paid = Amount::from(1);
quote.update_state_from_amounts();
db.add_mint_quote(quote.clone()).await.unwrap();
let operation_id = uuid::Uuid::new_v4();
db.reserve_mint_quote("e.id, &operation_id)
.await
.unwrap();
let secrets = PreMintSecrets::from_seed(
keyset_id,
0,
&wallet.seed,
Amount::from(1),
&SplitTarget::None,
&FeeAndAmounts::from((0, vec![1])),
)
.unwrap();
db.add_saga(WalletSaga::new(
operation_id,
WalletSagaState::Issue(IssueSagaState::MintRequested),
Amount::from(1),
quote.mint_url.clone(),
quote.unit.clone(),
OperationData::Mint(MintOperationData::new_single(
quote.id.clone(),
Amount::from(1),
Some(0),
Some(1),
Some(secrets.blinded_messages()),
)),
))
.await
.unwrap();
let mut response = paid_response("e);
if let MintQuoteResponse::Bolt11(response) = &mut response {
response.amount_paid = Amount::from(1);
response.amount_issued = Amount::from(1);
response.state = MintQuoteState::Issued;
}
client.set_mint_quote_status_response("e.id, response);
let returned = wallet.check_mint_quote_status("e.id).await.unwrap();
let stored = db.get_mint_quote("e.id).await.unwrap().unwrap();
assert_eq!(returned, stored);
assert_eq!(returned.version, 2); assert_eq!(returned.amount_paid, Amount::from(1));
assert_eq!(returned.amount_issued, Amount::from(1));
assert_eq!(returned.state, MintQuoteState::Issued);
assert!(db.get_saga(&operation_id).await.unwrap().is_none());
assert_eq!(wallet.total_balance().await.unwrap(), Amount::from(1));
let returned = wallet.check_mint_quote_status("e.id).await.unwrap();
assert_eq!(
returned,
db.get_mint_quote("e.id).await.unwrap().unwrap()
);
assert_eq!(returned.used_by_operation, None);
assert_eq!(returned.amount_issued, Amount::from(1));
assert_eq!(wallet.total_balance().await.unwrap(), Amount::from(1));
}
#[tokio::test]
async fn quote_status_propagates_reload_failures() {
for missing in [true, false] {
for api in ["single", "batch", "fetch", "all"] {
let path = std::env::temp_dir()
.join(format!("cdk-quote-status-{}.sqlite", uuid::Uuid::new_v4()));
let db = Arc::new(cdk_sqlite::WalletSqliteDatabase::new(&path).await.unwrap());
let client = Arc::new(MockMintConnector::new());
let wallet = create_test_wallet_with_mock(db.clone(), client.clone()).await;
let quote = test_mint_quote(test_mint_url());
db.add_mint_quote(quote.clone()).await.unwrap();
client.set_mint_quote_status_response("e.id, paid_response("e));
client
.push_post_batch_check_mint_quote_status_response(Ok(vec![paid_response("e)]));
let connection = rusqlite::Connection::open(&path).unwrap();
let action = match missing {
true => "DELETE FROM mint_quote WHERE id = NEW.id;",
false => "UPDATE mint_quote SET state = 'invalid-state' WHERE id = NEW.id;",
};
connection
.execute_batch(&format!(
"CREATE TRIGGER fail_reload AFTER UPDATE OF version ON mint_quote
BEGIN {action} END;"
))
.unwrap();
let result = match api {
"single" => wallet.check_mint_quote_status("e.id).await,
"batch" => wallet
.batch_check_mint_quote_status(&["e.id])
.await
.map(|mut quotes| quotes.remove(0)),
"fetch" => wallet.fetch_mint_quote("e.id, None).await,
_ => {
assert!(wallet.check_all_mint_quotes().await.unwrap().is_empty());
Err(Error::UnknownQuote)
}
};
if api != "all" {
match missing {
true => assert!(matches!(result, Err(Error::UnknownQuote)), "{result:?}"),
false => assert!(matches!(result, Err(Error::Database(_))), "{result:?}"),
}
}
let versions: Vec<u32> = connection
.prepare("SELECT version FROM mint_quote")
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(versions, if missing { vec![] } else { vec![1] });
drop(wallet);
drop(db);
drop(connection);
std::fs::remove_file(path).unwrap();
}
}
}