#![allow(clippy::unwrap_used, clippy::missing_panics_doc)]
use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use bitcoin::bip32::DerivationPath;
use cashu::nut00::KnownMethod;
use cashu::secret::Secret;
use cashu::{Amount, CurrencyUnit, MeltQuoteState, MintQuoteState, SecretKey};
use web_time::{SystemTime, UNIX_EPOCH};
use super::*;
use crate::mint_url::MintUrl;
use crate::nuts::{Id, KeySetInfo, Keys, MintInfo, Proof, State};
use crate::wallet::{
MeltQuote, MintQuote, OperationData, ProofInfo, SwapOperationData, SwapSagaState, Transaction,
TransactionDirection, WalletSaga, WalletSagaState,
};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn unique_id() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("test_{}_{}", now, n)
}
fn test_keys_with_id() -> (Keys, Id) {
let mut keys_map = BTreeMap::new();
let secret_bytes: [[u8; 32]; 4] = [[1u8; 32], [2u8; 32], [4u8; 32], [8u8; 32]];
for (i, amount) in [1u64, 2, 4, 8].iter().enumerate() {
let sk = SecretKey::from_slice(&secret_bytes[i]).expect("valid secret key");
let pk = sk.public_key();
keys_map.insert(Amount::from(*amount), pk);
}
let keys = Keys::new(keys_map);
let id = Id::v1_from_keys(&keys);
(keys, id)
}
fn test_keyset_id() -> Id {
Id::from_str("00916bbf7ef91a36").unwrap()
}
fn test_keyset_id_2() -> Id {
Id::from_str("00916bbf7ef91a37").unwrap()
}
fn test_mint_url() -> MintUrl {
MintUrl::from_str("https://test-mint.example.com").unwrap()
}
fn test_mint_url_2() -> MintUrl {
MintUrl::from_str("https://test-mint-2.example.com").unwrap()
}
fn test_keyset_info(keyset_id: Id, _mint_url: &MintUrl) -> KeySetInfo {
KeySetInfo {
id: keyset_id,
unit: CurrencyUnit::Sat,
active: true,
input_fee_ppk: 0,
final_expiry: None,
}
}
fn test_proof(keyset_id: Id, amount: u64) -> Proof {
Proof {
amount: Amount::from(amount),
keyset_id,
secret: Secret::generate(),
c: SecretKey::generate().public_key(),
witness: None,
dleq: None,
p2pk_e: None,
}
}
fn test_proof_info(keyset_id: Id, amount: u64, mint_url: MintUrl) -> ProofInfo {
let proof = test_proof(keyset_id, amount);
ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap()
}
fn test_mint_quote(mint_url: MintUrl) -> MintQuote {
MintQuote::new(
unique_id(),
mint_url,
cashu::PaymentMethod::Known(KnownMethod::Bolt11),
Some(Amount::from(1000)),
CurrencyUnit::Sat,
"lnbc1000...".to_string(),
9999999999,
None,
)
}
fn test_melt_quote() -> MeltQuote {
MeltQuote {
id: unique_id(),
mint_url: Some(test_mint_url()),
unit: CurrencyUnit::Sat,
amount: Amount::from(1000),
request: "lnbc1000...".to_string(),
fee_reserve: Amount::from(10),
state: cashu::MeltQuoteState::Unpaid,
expiry: 9999999999,
payment_preimage: None,
payment_method: cashu::PaymentMethod::Known(KnownMethod::Bolt11),
used_by_operation: None,
version: 0,
}
}
fn test_transaction(mint_url: MintUrl, direction: TransactionDirection) -> Transaction {
let ys = vec![SecretKey::generate().public_key()];
Transaction {
mint_url,
direction,
amount: Amount::from(100),
fee: Amount::from(1),
unit: CurrencyUnit::Sat,
ys,
timestamp: 1234567890,
memo: Some("test transaction".to_string()),
metadata: HashMap::new(),
quote_id: None,
payment_request: None,
payment_proof: None,
payment_method: None,
saga_id: None,
}
}
fn test_wallet_saga(mint_url: MintUrl) -> WalletSaga {
WalletSaga::new(
uuid::Uuid::new_v4(),
WalletSagaState::Swap(SwapSagaState::ProofsReserved),
Amount::from(1000),
mint_url,
CurrencyUnit::Sat,
OperationData::Swap(SwapOperationData {
input_amount: Amount::from(1000),
output_amount: Amount::from(990),
counter_start: Some(0),
counter_end: Some(10),
blinded_messages: None,
}),
)
}
pub async fn add_and_get_mint<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let mint_info = MintInfo::default();
db.add_mint(mint_url.clone(), Some(mint_info.clone()))
.await
.unwrap();
let retrieved = db.get_mint(mint_url.clone()).await.unwrap();
assert!(retrieved.is_some());
let mints = db.get_mints().await.unwrap();
assert!(mints.contains_key(&mint_url));
}
pub async fn add_mint_without_info<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
db.add_mint(mint_url.clone(), None).await.unwrap();
let mints = db.get_mints().await.unwrap();
assert!(mints.contains_key(&mint_url));
}
pub async fn remove_mint<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
db.add_mint(mint_url.clone(), None).await.unwrap();
db.remove_mint(mint_url.clone()).await.unwrap();
let result = db.get_mint(mint_url).await.unwrap();
assert!(result.is_none());
}
pub async fn update_mint_url<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let old_url = test_mint_url();
let new_url = test_mint_url_2();
db.add_mint(old_url.clone(), None).await.unwrap();
db.update_mint_url(old_url.clone(), new_url.clone())
.await
.unwrap();
}
pub async fn add_and_get_keysets<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let keyset_info = test_keyset_info(keyset_id, &mint_url);
db.add_mint(mint_url.clone(), None).await.unwrap();
db.add_mint_keysets(mint_url.clone(), vec![keyset_info.clone()])
.await
.unwrap();
let retrieved = db.get_keyset_by_id(&keyset_id).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().id, keyset_id);
let keysets = db.get_mint_keysets(mint_url).await.unwrap();
assert!(keysets.is_some());
assert!(!keysets.unwrap().is_empty());
}
pub async fn get_keyset_by_id_in_transaction<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let keyset_info = test_keyset_info(keyset_id, &mint_url);
db.add_mint(mint_url.clone(), None).await.unwrap();
db.add_mint_keysets(mint_url.clone(), vec![keyset_info])
.await
.unwrap();
let retrieved = db.get_keyset_by_id(&keyset_id).await.unwrap();
assert!(retrieved.is_some());
}
pub async fn add_and_get_keys<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let (keys, keyset_id) = test_keys_with_id();
let keyset = cashu::KeySet {
id: keyset_id,
unit: CurrencyUnit::Sat,
active: None,
keys: keys.clone(),
input_fee_ppk: 0,
final_expiry: None,
};
db.add_keys(keyset).await.unwrap();
let retrieved = db.get_keys(&keyset_id).await.unwrap();
assert!(retrieved.is_some());
let retrieved_keys = retrieved.unwrap();
assert_eq!(retrieved_keys.len(), keys.len());
}
pub async fn get_keys_in_transaction<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let (keys, keyset_id) = test_keys_with_id();
let keyset = cashu::KeySet {
id: keyset_id,
unit: CurrencyUnit::Sat,
active: None,
keys: keys.clone(),
input_fee_ppk: 0,
final_expiry: None,
};
db.add_keys(keyset).await.unwrap();
let retrieved = db.get_keys(&keyset_id).await.unwrap();
assert!(retrieved.is_some());
let retrieved_keys = retrieved.unwrap();
assert_eq!(retrieved_keys.len(), keys.len());
}
pub async fn remove_keys<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let (keys, keyset_id) = test_keys_with_id();
let keyset = cashu::KeySet {
id: keyset_id,
unit: CurrencyUnit::Sat,
active: None,
keys,
input_fee_ppk: 0,
final_expiry: None,
};
db.add_keys(keyset).await.unwrap();
let retrieved = db.get_keys(&keyset_id).await.unwrap();
assert!(retrieved.is_some());
db.remove_keys(&keyset_id).await.unwrap();
let retrieved = db.get_keys(&keyset_id).await.unwrap();
assert!(retrieved.is_none());
}
pub async fn add_and_get_mint_quote<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let quote = test_mint_quote(mint_url);
db.add_mint_quote(quote.clone()).await.unwrap();
let retrieved = db.get_mint_quote("e.id).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().id, quote.id);
let quotes = db.get_mint_quotes().await.unwrap();
assert!(!quotes.is_empty());
}
pub async fn get_mint_quote_in_transaction<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let quote = test_mint_quote(mint_url);
db.add_mint_quote(quote.clone()).await.unwrap();
let retrieved = db.get_mint_quote("e.id).await.unwrap();
assert!(retrieved.is_some());
}
pub async fn remove_mint_quote<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let quote = test_mint_quote(mint_url);
db.add_mint_quote(quote.clone()).await.unwrap();
db.remove_mint_quote("e.id).await.unwrap();
let retrieved = db.get_mint_quote("e.id).await.unwrap();
assert!(retrieved.is_none());
}
pub async fn add_and_get_melt_quote<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let quote = test_melt_quote();
db.add_melt_quote(quote.clone()).await.unwrap();
let retrieved = db.get_melt_quote("e.id).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().id, quote.id);
let quotes = db.get_melt_quotes().await.unwrap();
assert!(!quotes.is_empty());
}
pub async fn get_melt_quote_in_transaction<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let quote = test_melt_quote();
db.add_melt_quote(quote.clone()).await.unwrap();
let retrieved = db.get_melt_quote("e.id).await.unwrap();
assert!(retrieved.is_some());
}
pub async fn remove_melt_quote<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let quote = test_melt_quote();
db.add_melt_quote(quote.clone()).await.unwrap();
db.remove_melt_quote("e.id).await.unwrap();
let retrieved = db.get_melt_quote("e.id).await.unwrap();
assert!(retrieved.is_none());
}
pub async fn add_mint_quote_optimistic_locking<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let quote = test_mint_quote(mint_url);
db.add_mint_quote(quote.clone()).await.unwrap();
let retrieved = db.get_mint_quote("e.id).await.unwrap().unwrap();
assert_eq!(retrieved.version, 0);
let mut quote_update_1 = quote.clone();
quote_update_1.state = MintQuoteState::Issued; db.add_mint_quote(quote_update_1.clone()).await.unwrap();
let retrieved = db.get_mint_quote("e.id).await.unwrap().unwrap();
assert_eq!(retrieved.version, 1);
assert_eq!(retrieved.state, MintQuoteState::Issued);
let mut stale_quote = quote_update_1.clone();
stale_quote.amount = Some(Amount::from(999));
let result = db.add_mint_quote(stale_quote).await;
assert!(matches!(
result,
Err(crate::database::Error::ConcurrentUpdate)
));
let retrieved = db.get_mint_quote("e.id).await.unwrap().unwrap();
assert_eq!(retrieved.version, 1);
assert_eq!(retrieved.state, MintQuoteState::Issued);
assert_ne!(retrieved.amount, Some(Amount::from(999)));
}
pub async fn add_melt_quote_optimistic_locking<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let quote = test_melt_quote();
db.add_melt_quote(quote.clone()).await.unwrap();
let retrieved = db.get_melt_quote("e.id).await.unwrap().unwrap();
assert_eq!(retrieved.version, 0);
let mut quote_update_1 = quote.clone();
quote_update_1.state = MeltQuoteState::Paid;
db.add_melt_quote(quote_update_1.clone()).await.unwrap();
let retrieved = db.get_melt_quote("e.id).await.unwrap().unwrap();
assert_eq!(retrieved.version, 1);
assert_eq!(retrieved.state, MeltQuoteState::Paid);
let mut stale_quote = quote_update_1.clone();
stale_quote.amount = Amount::from(999);
let result = db.add_melt_quote(stale_quote).await;
assert!(matches!(
result,
Err(crate::database::Error::ConcurrentUpdate)
));
let retrieved = db.get_melt_quote("e.id).await.unwrap().unwrap();
assert_eq!(retrieved.version, 1);
assert_eq!(retrieved.state, MeltQuoteState::Paid);
assert_ne!(retrieved.amount, Amount::from(999));
}
pub async fn add_and_get_proofs<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
db.update_proofs(vec![proof_info.clone()], vec![])
.await
.unwrap();
let proofs = db.get_proofs(None, None, None, None).await.unwrap();
assert!(!proofs.is_empty());
let proofs = db
.get_proofs(Some(mint_url.clone()), None, None, None)
.await
.unwrap();
assert!(!proofs.is_empty());
let ys = vec![proof_info.y];
let proofs = db.get_proofs_by_ys(ys).await.unwrap();
assert!(!proofs.is_empty());
}
pub async fn get_proofs_in_transaction<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
db.update_proofs(vec![proof_info.clone()], vec![])
.await
.unwrap();
let proofs = db.get_proofs(None, None, None, None).await.unwrap();
assert!(!proofs.is_empty());
}
pub async fn update_proofs<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info_1 = test_proof_info(keyset_id, 100, mint_url.clone());
let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone());
db.update_proofs(vec![proof_info_1.clone()], vec![])
.await
.unwrap();
db.update_proofs(vec![proof_info_2.clone()], vec![proof_info_1.y])
.await
.unwrap();
let proofs = db.get_proofs(None, None, None, None).await.unwrap();
assert_eq!(proofs.len(), 1);
assert_eq!(proofs[0].y, proof_info_2.y);
}
pub async fn update_proofs_state<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
db.update_proofs(vec![proof_info.clone()], vec![])
.await
.unwrap();
db.update_proofs_state(vec![proof_info.y], State::Pending)
.await
.unwrap();
let proofs = db
.get_proofs(None, None, Some(vec![State::Pending]), None)
.await
.unwrap();
assert!(!proofs.is_empty());
}
pub async fn filter_proofs_by_unit<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
db.update_proofs(vec![proof_info.clone()], vec![])
.await
.unwrap();
let proofs = db
.get_proofs(None, Some(CurrencyUnit::Sat), None, None)
.await
.unwrap();
assert!(!proofs.is_empty());
let proofs = db
.get_proofs(None, Some(CurrencyUnit::Msat), None, None)
.await
.unwrap();
assert!(proofs.is_empty());
}
pub async fn filter_proofs_by_state<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
db.update_proofs(vec![proof_info.clone()], vec![])
.await
.unwrap();
let proofs = db
.get_proofs(None, None, Some(vec![State::Unspent]), None)
.await
.unwrap();
assert!(!proofs.is_empty());
let proofs = db
.get_proofs(None, None, Some(vec![State::Spent]), None)
.await
.unwrap();
assert!(proofs.is_empty());
}
pub async fn get_balance<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info_1 = test_proof_info(keyset_id, 100, mint_url.clone());
let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone());
db.update_proofs(vec![proof_info_1, proof_info_2], vec![])
.await
.unwrap();
let balance = db.get_balance(None, None, None).await.unwrap();
assert_eq!(balance, 300);
let balance = db.get_balance(Some(mint_url), None, None).await.unwrap();
assert_eq!(balance, 300);
}
pub async fn get_balance_by_state<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
db.update_proofs(vec![proof_info.clone()], vec![])
.await
.unwrap();
let balance = db
.get_balance(None, None, Some(vec![State::Unspent]))
.await
.unwrap();
assert_eq!(balance, 100);
let balance = db
.get_balance(None, None, Some(vec![State::Spent]))
.await
.unwrap();
assert_eq!(balance, 0);
}
pub async fn increment_keyset_counter<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let keyset_id = test_keyset_id();
let counter1 = db.increment_keyset_counter(&keyset_id, 5).await.unwrap();
assert_eq!(counter1, 5);
let counter2 = db.increment_keyset_counter(&keyset_id, 10).await.unwrap();
assert_eq!(counter2, 15);
}
pub async fn keyset_counter_isolation<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let keyset_id_1 = test_keyset_id();
let keyset_id_2 = test_keyset_id_2();
db.increment_keyset_counter(&keyset_id_1, 5).await.unwrap();
let counter2 = db.increment_keyset_counter(&keyset_id_2, 10).await.unwrap();
assert_eq!(counter2, 10);
let counter1 = db.increment_keyset_counter(&keyset_id_1, 0).await.unwrap();
assert_eq!(counter1, 5);
}
pub async fn add_and_get_transaction<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let transaction = test_transaction(mint_url.clone(), TransactionDirection::Incoming);
let tx_id = transaction.id();
db.add_transaction(transaction.clone()).await.unwrap();
let retrieved = db.get_transaction(tx_id).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().id(), tx_id);
}
pub async fn list_transactions<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let tx_incoming = test_transaction(mint_url.clone(), TransactionDirection::Incoming);
let tx_outgoing = test_transaction(mint_url.clone(), TransactionDirection::Outgoing);
db.add_transaction(tx_incoming).await.unwrap();
db.add_transaction(tx_outgoing).await.unwrap();
let transactions = db.list_transactions(None, None, None).await.unwrap();
assert_eq!(transactions.len(), 2);
let incoming = db
.list_transactions(None, Some(TransactionDirection::Incoming), None)
.await
.unwrap();
assert_eq!(incoming.len(), 1);
let outgoing = db
.list_transactions(None, Some(TransactionDirection::Outgoing), None)
.await
.unwrap();
assert_eq!(outgoing.len(), 1);
}
pub async fn filter_transactions_by_mint<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url_1 = test_mint_url();
let mint_url_2 = test_mint_url_2();
let tx_1 = test_transaction(mint_url_1.clone(), TransactionDirection::Incoming);
let tx_2 = test_transaction(mint_url_2.clone(), TransactionDirection::Incoming);
db.add_transaction(tx_1).await.unwrap();
db.add_transaction(tx_2).await.unwrap();
let transactions = db
.list_transactions(Some(mint_url_1), None, None)
.await
.unwrap();
assert_eq!(transactions.len(), 1);
}
pub async fn remove_transaction<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let transaction = test_transaction(mint_url, TransactionDirection::Incoming);
let tx_id = transaction.id();
db.add_transaction(transaction).await.unwrap();
db.remove_transaction(tx_id).await.unwrap();
let retrieved = db.get_transaction(tx_id).await.unwrap();
assert!(retrieved.is_none());
}
pub async fn kvstore_write_and_read<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
db.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
.await
.unwrap();
db.kv_write("test_namespace", "sub_namespace", "key2", b"value2")
.await
.unwrap();
db.kv_write("test_namespace", "other_sub", "key3", b"value3")
.await
.unwrap();
let value1 = db
.kv_read("test_namespace", "sub_namespace", "key1")
.await
.unwrap();
assert_eq!(value1, Some(b"value1".to_vec()));
let value2 = db
.kv_read("test_namespace", "sub_namespace", "key2")
.await
.unwrap();
assert_eq!(value2, Some(b"value2".to_vec()));
let value3 = db
.kv_read("test_namespace", "other_sub", "key3")
.await
.unwrap();
assert_eq!(value3, Some(b"value3".to_vec()));
let missing = db
.kv_read("test_namespace", "sub_namespace", "missing")
.await
.unwrap();
assert_eq!(missing, None);
}
pub async fn kvstore_list<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
db.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
.await
.unwrap();
db.kv_write("test_namespace", "sub_namespace", "key2", b"value2")
.await
.unwrap();
db.kv_write("test_namespace", "other_sub", "key3", b"value3")
.await
.unwrap();
let mut keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap();
keys.sort();
assert_eq!(keys, vec!["key1", "key2"]);
let other_keys = db.kv_list("test_namespace", "other_sub").await.unwrap();
assert_eq!(other_keys, vec!["key3"]);
let empty_keys = db.kv_list("test_namespace", "empty_sub").await.unwrap();
assert!(empty_keys.is_empty());
}
pub async fn kvstore_update<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
db.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
.await
.unwrap();
let value = db
.kv_read("test_namespace", "sub_namespace", "key1")
.await
.unwrap();
assert_eq!(value, Some(b"value1".to_vec()));
db.kv_write("test_namespace", "sub_namespace", "key1", b"updated_value1")
.await
.unwrap();
let value = db
.kv_read("test_namespace", "sub_namespace", "key1")
.await
.unwrap();
assert_eq!(value, Some(b"updated_value1".to_vec()));
}
pub async fn kvstore_remove<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
db.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
.await
.unwrap();
db.kv_write("test_namespace", "sub_namespace", "key2", b"value2")
.await
.unwrap();
let keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap();
assert_eq!(keys.len(), 2);
db.kv_remove("test_namespace", "sub_namespace", "key1")
.await
.unwrap();
let value = db
.kv_read("test_namespace", "sub_namespace", "key1")
.await
.unwrap();
assert_eq!(value, None);
let value = db
.kv_read("test_namespace", "sub_namespace", "key2")
.await
.unwrap();
assert_eq!(value, Some(b"value2".to_vec()));
let keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap();
assert_eq!(keys, vec!["key2"]);
}
pub async fn kvstore_namespace_isolation<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
db.kv_write("ns1", "sub", "key", b"value_ns1")
.await
.unwrap();
db.kv_write("ns2", "sub", "key", b"value_ns2")
.await
.unwrap();
db.kv_write("ns1", "sub2", "key", b"value_sub2")
.await
.unwrap();
let value1 = db.kv_read("ns1", "sub", "key").await.unwrap();
assert_eq!(value1, Some(b"value_ns1".to_vec()));
let value2 = db.kv_read("ns2", "sub", "key").await.unwrap();
assert_eq!(value2, Some(b"value_ns2".to_vec()));
let value3 = db.kv_read("ns1", "sub2", "key").await.unwrap();
assert_eq!(value3, Some(b"value_sub2".to_vec()));
db.kv_remove("ns1", "sub", "key").await.unwrap();
let value1 = db.kv_read("ns1", "sub", "key").await.unwrap();
assert_eq!(value1, None);
let value2 = db.kv_read("ns2", "sub", "key").await.unwrap();
assert_eq!(value2, Some(b"value_ns2".to_vec()));
let value3 = db.kv_read("ns1", "sub2", "key").await.unwrap();
assert_eq!(value3, Some(b"value_sub2".to_vec()));
}
pub async fn add_and_get_p2pk_key<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let pubkey = SecretKey::generate().public_key();
let derivation_path = DerivationPath::from_str("m/0'/0'/0'").unwrap();
let derivation_index = 0u32;
db.add_p2pk_key(&pubkey, derivation_path.clone(), derivation_index)
.await
.unwrap();
let retrieved = db.get_p2pk_key(&pubkey).await.unwrap();
assert!(retrieved.is_some());
let retrieved_key = retrieved.unwrap();
assert_eq!(retrieved_key.pubkey, pubkey);
assert_eq!(retrieved_key.derivation_path, derivation_path);
assert_eq!(retrieved_key.derivation_index, derivation_index);
let non_existent_pubkey = SecretKey::generate().public_key();
let result = db.get_p2pk_key(&non_existent_pubkey).await.unwrap();
assert!(result.is_none());
}
pub async fn list_p2pk_keys_empty<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let keys = db.list_p2pk_keys().await.unwrap();
assert!(keys.is_empty());
}
pub async fn list_p2pk_keys_multiple<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let pubkey1 = SecretKey::generate().public_key();
let pubkey2 = SecretKey::generate().public_key();
let pubkey3 = SecretKey::generate().public_key();
db.add_p2pk_key(&pubkey1, DerivationPath::from_str("m/0'/0'/0'").unwrap(), 0)
.await
.unwrap();
db.add_p2pk_key(&pubkey2, DerivationPath::from_str("m/0'/0'/1'").unwrap(), 1)
.await
.unwrap();
db.add_p2pk_key(&pubkey3, DerivationPath::from_str("m/0'/0'/2'").unwrap(), 2)
.await
.unwrap();
let keys = db.list_p2pk_keys().await.unwrap();
assert_eq!(keys.len(), 3);
let pubkeys: Vec<_> = keys.iter().map(|k| k.pubkey).collect();
assert!(pubkeys.contains(&pubkey1));
assert!(pubkeys.contains(&pubkey2));
assert!(pubkeys.contains(&pubkey3));
let derivation_indices: Vec<_> = keys.iter().map(|k| k.derivation_index).collect();
assert!(derivation_indices.contains(&0));
assert!(derivation_indices.contains(&1));
assert!(derivation_indices.contains(&2));
}
pub async fn latest_p2pk_empty<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let latest = db.latest_p2pk().await.unwrap();
assert!(latest.is_none());
}
pub async fn latest_p2pk_with_keys<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let pubkey1 = SecretKey::generate().public_key();
let pubkey2 = SecretKey::generate().public_key();
let pubkey3 = SecretKey::generate().public_key();
db.add_p2pk_key(&pubkey1, DerivationPath::from_str("m/0'/0'/0'").unwrap(), 0)
.await
.unwrap();
db.add_p2pk_key(&pubkey2, DerivationPath::from_str("m/0'/0'/1'").unwrap(), 1)
.await
.unwrap();
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
db.add_p2pk_key(&pubkey3, DerivationPath::from_str("m/0'/0'/2'").unwrap(), 2)
.await
.unwrap();
let latest = db.latest_p2pk().await.unwrap();
assert!(latest.is_some());
let latest_key = latest.unwrap();
assert_eq!(latest_key.pubkey, pubkey3);
assert_eq!(latest_key.derivation_index, 2);
}
pub async fn add_and_get_saga<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let saga = test_wallet_saga(mint_url);
let saga_id = saga.id;
db.add_saga(saga.clone()).await.unwrap();
let retrieved = db.get_saga(&saga_id).await.unwrap();
assert!(retrieved.is_some());
let retrieved = retrieved.unwrap();
assert_eq!(retrieved.id, saga_id);
assert_eq!(retrieved.version, 0);
assert_eq!(retrieved.amount, Amount::from(1000));
}
pub async fn update_saga_optimistic_locking<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let saga = test_wallet_saga(mint_url);
let saga_id = saga.id;
db.add_saga(saga).await.unwrap();
let mut saga = db.get_saga(&saga_id).await.unwrap().unwrap();
assert_eq!(saga.version, 0);
saga.update_state(WalletSagaState::Swap(SwapSagaState::SwapRequested));
assert_eq!(saga.version, 1);
let success = db.update_saga(saga.clone()).await.unwrap();
assert!(success);
let retrieved = db.get_saga(&saga_id).await.unwrap().unwrap();
assert_eq!(retrieved.version, 1);
assert!(matches!(
retrieved.state,
WalletSagaState::Swap(SwapSagaState::SwapRequested)
));
let mut stale_saga = saga.clone();
stale_saga.version = 0; stale_saga.update_state(WalletSagaState::Swap(SwapSagaState::ProofsReserved));
let success = db.update_saga(stale_saga).await.unwrap();
assert!(!success);
let retrieved = db.get_saga(&saga_id).await.unwrap().unwrap();
assert_eq!(retrieved.version, 1);
assert!(matches!(
retrieved.state,
WalletSagaState::Swap(SwapSagaState::SwapRequested)
));
}
pub async fn delete_saga<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let saga = test_wallet_saga(mint_url);
let saga_id = saga.id;
db.add_saga(saga).await.unwrap();
let retrieved = db.get_saga(&saga_id).await.unwrap();
assert!(retrieved.is_some());
db.delete_saga(&saga_id).await.unwrap();
let retrieved = db.get_saga(&saga_id).await.unwrap();
assert!(retrieved.is_none());
}
pub async fn get_incomplete_sagas<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let saga1 = test_wallet_saga(mint_url.clone());
let saga2 = test_wallet_saga(mint_url.clone());
let saga3 = test_wallet_saga(mint_url);
let saga1_id = saga1.id;
let saga3_id = saga3.id;
db.add_saga(saga1).await.unwrap();
db.add_saga(saga2.clone()).await.unwrap();
db.add_saga(saga3).await.unwrap();
let incomplete = db.get_incomplete_sagas().await.unwrap();
assert_eq!(incomplete.len(), 3);
db.delete_saga(&saga2.id).await.unwrap();
let incomplete = db.get_incomplete_sagas().await.unwrap();
assert_eq!(incomplete.len(), 2);
let ids: Vec<_> = incomplete.iter().map(|s| s.id).collect();
assert!(ids.contains(&saga1_id));
assert!(ids.contains(&saga3_id));
}
pub async fn reserve_proofs<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info_1 = test_proof_info(keyset_id, 100, mint_url.clone());
let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone());
db.update_proofs(vec![proof_info_1.clone(), proof_info_2.clone()], vec![])
.await
.unwrap();
let operation_id = uuid::Uuid::new_v4();
db.reserve_proofs(vec![proof_info_1.y, proof_info_2.y], &operation_id)
.await
.unwrap();
let proofs = db
.get_proofs(None, None, Some(vec![State::Reserved]), None)
.await
.unwrap();
assert_eq!(proofs.len(), 2);
}
pub async fn release_proofs<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info_1 = test_proof_info(keyset_id, 100, mint_url.clone());
let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone());
db.update_proofs(vec![proof_info_1.clone(), proof_info_2.clone()], vec![])
.await
.unwrap();
let operation_id = uuid::Uuid::new_v4();
db.reserve_proofs(vec![proof_info_1.y, proof_info_2.y], &operation_id)
.await
.unwrap();
let reserved = db
.get_proofs(None, None, Some(vec![State::Reserved]), None)
.await
.unwrap();
assert_eq!(reserved.len(), 2);
db.release_proofs(&operation_id).await.unwrap();
let unspent = db
.get_proofs(None, None, Some(vec![State::Unspent]), None)
.await
.unwrap();
assert_eq!(unspent.len(), 2);
let reserved = db
.get_proofs(None, None, Some(vec![State::Reserved]), None)
.await
.unwrap();
assert!(reserved.is_empty());
}
pub async fn get_reserved_proofs<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info_1 = test_proof_info(keyset_id, 100, mint_url.clone());
let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone());
let proof_info_3 = test_proof_info(keyset_id, 300, mint_url.clone());
db.update_proofs(
vec![
proof_info_1.clone(),
proof_info_2.clone(),
proof_info_3.clone(),
],
vec![],
)
.await
.unwrap();
let operation_id_1 = uuid::Uuid::new_v4();
db.reserve_proofs(vec![proof_info_1.y, proof_info_2.y], &operation_id_1)
.await
.unwrap();
let operation_id_2 = uuid::Uuid::new_v4();
db.reserve_proofs(vec![proof_info_3.y], &operation_id_2)
.await
.unwrap();
let reserved_1 = db.get_reserved_proofs(&operation_id_1).await.unwrap();
assert_eq!(reserved_1.len(), 2);
let ys_1: Vec<_> = reserved_1.iter().map(|p| p.y).collect();
assert!(ys_1.contains(&proof_info_1.y));
assert!(ys_1.contains(&proof_info_2.y));
let reserved_2 = db.get_reserved_proofs(&operation_id_2).await.unwrap();
assert_eq!(reserved_2.len(), 1);
assert_eq!(reserved_2[0].y, proof_info_3.y);
let empty = db.get_reserved_proofs(&uuid::Uuid::new_v4()).await.unwrap();
assert!(empty.is_empty());
}
pub async fn reserve_proofs_already_reserved<DB>(db: DB)
where
DB: Database<crate::database::Error>,
{
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
db.update_proofs(vec![proof_info.clone()], vec![])
.await
.unwrap();
let operation_id_1 = uuid::Uuid::new_v4();
db.reserve_proofs(vec![proof_info.y], &operation_id_1)
.await
.unwrap();
let operation_id_2 = uuid::Uuid::new_v4();
let result = db.reserve_proofs(vec![proof_info.y], &operation_id_2).await;
assert!(result.is_err());
}
#[macro_export]
macro_rules! wallet_db_test {
($make_db_fn:ident) => {
wallet_db_test!(
$make_db_fn,
add_and_get_mint,
add_mint_without_info,
remove_mint,
update_mint_url,
add_and_get_keysets,
get_keyset_by_id_in_transaction,
add_and_get_keys,
get_keys_in_transaction,
remove_keys,
add_and_get_mint_quote,
get_mint_quote_in_transaction,
remove_mint_quote,
add_and_get_melt_quote,
get_melt_quote_in_transaction,
remove_melt_quote,
add_mint_quote_optimistic_locking,
add_melt_quote_optimistic_locking,
add_and_get_proofs,
get_proofs_in_transaction,
update_proofs,
update_proofs_state,
filter_proofs_by_unit,
filter_proofs_by_state,
get_balance,
get_balance_by_state,
increment_keyset_counter,
keyset_counter_isolation,
add_and_get_transaction,
list_transactions,
filter_transactions_by_mint,
remove_transaction,
kvstore_write_and_read,
kvstore_list,
kvstore_update,
kvstore_remove,
kvstore_namespace_isolation,
add_and_get_saga,
update_saga_optimistic_locking,
delete_saga,
get_incomplete_sagas,
reserve_proofs,
release_proofs,
get_reserved_proofs,
reserve_proofs_already_reserved
);
};
($make_db_fn:ident, $($name:ident),+ $(,)?) => {
::paste::paste! {
$(
#[tokio::test]
async fn [<wallet_ $name>]() {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
cdk_common::database::wallet::test::$name($make_db_fn(format!("test_{}_{}", now.as_nanos(), stringify!($name))).await).await;
}
)+
}
};
}