# Migration Guide: v0.14.x to v0.15.0 ## Summary **Two major breaking changes:** 1. **`MultiMintWallet` removed** → Use `WalletRepository` to get individual `Wallet` instances 2. **Melt saga pattern** → `confirm()` returns `MeltOutcome` (Paid or Pending), with async support **Key Changes:** - `wallet.mint_quote(&mint_url, ...)` → `wallet.get_wallet(&mint_url, &unit)?.mint_quote(...)` - `prepared_melt.confirm().await?` → `match prepared_melt.confirm().await? { MeltOutcome::Paid(_) => ..., MeltOutcome::Pending(_) => ... }` ## Quick Reference | Operation | Before | After | |-----------|--------|-------| | **Create** | `MultiMintWallet::new(localstore, seed, unit)` | `WalletRepositoryBuilder::new().localstore(localstore).seed(seed).build()` | | **Add Mint** | `wallet.add_mint(mint_url)` | `wallet.add_wallet(mint_url)` | | **Get Wallet** | N/A (methods took mint_url) | `wallet.get_wallet(&mint_url, &unit)` | | **Mint Quote** | `wallet.mint_quote(&mint_url, method, amount, ...)` | `wallet.get_wallet(&mint_url, &unit)?.mint_quote(method, amount, ...)` | | **Balances** | `HashMap` | `BTreeMap` | | **Total** | `Amount` | `BTreeMap` | ## Detailed Changes ### 1. Construction ```rust // Before let wallet = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat).await?; // After let wallet = WalletRepositoryBuilder::new() .localstore(localstore) .seed(seed) .build() .await?; ``` ### 2. Adding Mints ```rust // Before wallet.add_mint(mint_url.clone()).await?; // After - creates wallets for ALL supported units wallet.add_wallet(mint_url.clone()).await?; // Or create specific wallet with config let config = WalletConfig::new() .with_target_proof_count(5) .with_metadata_cache_ttl(Some(Duration::from_secs(3600))); let mint_wallet = wallet .create_wallet(mint_url.clone(), CurrencyUnit::Sat, Some(config)) .await?; ``` ### 3. Method Calls (Get Wallet First) All methods that previously took `mint_url` now require getting the wallet first: ```rust // Before let quote = wallet .mint_quote(&mint_url, PaymentMethod::BOLT11, Some(amount), None, None) .await?; // After let mint_wallet = wallet.get_wallet(&mint_url, &CurrencyUnit::Sat).await?; let quote = mint_wallet .mint_quote(PaymentMethod::BOLT11, Some(amount), None, None) .await?; ``` This pattern applies to all wallet operations: - `mint_quote()` → get wallet → `mint_wallet.mint_quote()` - `wait_for_mint_quote()` → get wallet → `mint_wallet.wait_and_mint_quote()` - `melt_quote()` → get wallet → `mint_wallet.melt_quote()` - `prepare_melt()` → get wallet → `mint_wallet.prepare_melt()` - `prepare_send()` → get wallet → `mint_wallet.prepare_send()` - `receive()` → get wallet → `mint_wallet.receive()` - `swap()` → get wallet → `mint_wallet.swap()` - `restore()` → get wallet → `mint_wallet.restore()` - `mint_tokens()` → get wallet → `mint_wallet.mint()` (renamed) ### 4. wait_and_mint_quote Signature Change Note: `wait_and_mint_quote` now takes the full `MintQuote` instead of just the quote ID: ```rust // Before let proofs = wallet .wait_for_mint_quote(&mint_url, "e_id, split_target, conditions, timeout) .await?; // After - pass the full MintQuote let proofs = mint_wallet .wait_and_mint_quote(quote, split_target, conditions, timeout) .await?; ``` ### 5. Balance Handling ```rust // Before - single amount let total = wallet.total_balance().await?; // After - by currency unit let balances = wallet.total_balance().await?; let sat_total = balances .get(&CurrencyUnit::Sat) .copied() .unwrap_or(Amount::ZERO); ``` ```rust // Before - HashMap let balances: HashMap = wallet.get_balances().await?; for (mint_url, balance) in balances { println!("{}: {}", mint_url, balance); } // After - BTreeMap (includes unit) let balances: BTreeMap = wallet.get_balances().await?; for (key, balance) in balances { println!("{} ({}): {}", key.mint_url, key.unit, balance); } ``` ### 5. Receiving Tokens ```rust // Before let received = wallet.receive(&token_str, MultiMintReceiveOptions::default()).await?; // After let token = Token::from_str(&token_str)?; let token_data = wallet.get_token_data(&token).await?; let mint_wallet = wallet .get_wallet(&token_data.mint_url, &token_data.unit) .await?; let received = mint_wallet.receive(&token_str, ReceiveOptions::default()).await?; ``` ## API Changes ### Removed from MultiMintWallet All methods moved to `Wallet` (get wallet first, then call): | Method | Change | |--------|--------| | `mint_quote(mint_url, ...)` | → `Wallet::mint_quote(...)` | | `wait_for_mint_quote(mint_url, ...)` | → `Wallet::wait_and_mint_quote(...)` | | `melt_quote(mint_url, ...)` | → `Wallet::melt_quote(...)` | | `prepare_melt(mint_url, ...)` | → `Wallet::prepare_melt(...)` | | `prepare_send(mint_url, ...)` | → `Wallet::prepare_send(...)` | | `receive(...)` | → `Wallet::receive(...)` | | `restore(mint_url)` | → `Wallet::restore()` | | `swap(mint_url, ...)` | → `Wallet::swap(...)` | | `mint_tokens(mint_url, ...)` | → `Wallet::mint(...)` | | `melt_quote(mint_url, ...)` | → `Wallet::melt_quote(PaymentMethod, bolt11_invoice, ...)` | | `add_mint(mint_url)` | → `WalletRepository::add_wallet(mint_url)` | | `add_mint_with_config(...)` | → `WalletRepository::add_wallet_with_config(...)` | ### New WalletRepository Methods ```rust impl WalletRepository { // Get specific wallet async fn get_wallet(&self, mint_url: &MintUrl, unit: &CurrencyUnit) -> Result; // Get all wallets for a mint async fn get_wallets_for_mint(&self, mint_url: &MintUrl) -> Vec; // Check existence async fn has_wallet(&self, mint_url: &MintUrl, unit: &CurrencyUnit) -> bool; async fn has_mint(&self, mint_url: &MintUrl) -> bool; // Create with config async fn create_wallet(&self, mint_url: MintUrl, unit: CurrencyUnit, config: Option) -> Result; async fn set_mint_config(&self, mint_url: MintUrl, unit: CurrencyUnit, config: WalletConfig) -> Result; // Remove async fn remove_wallet(&self, mint_url: MintUrl, unit: CurrencyUnit) -> Result<()>; // Helpers async fn get_token_data(&self, token: &Token) -> Result; async fn check_all_mint_quotes(&self, mint_url: Option) -> Result; } ``` ### Type Changes | Old | New | |-----|-----| | `MultiMintWallet` | `WalletRepository` | | `MultiMintReceiveOptions` | `ReceiveOptions` | | `HashMap` (balances) | `BTreeMap` | | `Amount` (total_balance) | `BTreeMap` | ## Melt Saga Changes Melt operations now use the saga pattern with type-state safety and support for async completion. The API requires calling `melt_quote` first to create a quote, then `prepare_melt` with the quote ID. ### Basic Melt Flow ```rust // Before: prepare_melt returned a result that completed immediately let prepared = wallet.prepare_melt("e_id, HashMap::new()).await?; let result = prepared.confirm().await?; ``` ```rust // After: Two-step process - melt_quote then prepare_melt use cdk::nuts::PaymentMethod; use cdk::wallet::MeltOutcome; // Step 1: Create a melt quote let melt_quote = wallet .melt_quote(PaymentMethod::BOLT11, invoice, None, None) .await?; // Step 2: Prepare the melt with the quote ID let prepared = wallet .prepare_melt(&melt_quote.id, std::collections::HashMap::new()) .await?; // Step 3: Confirm and handle the outcome match prepared.confirm().await? { MeltOutcome::Paid(melted) => { println!("Paid! Amount: {}, Fee: {}", melted.amount(), melted.fee_paid()); } MeltOutcome::Pending(pending) => { // Melt is processing asynchronously // Can await immediately or drop for background completion let melted = pending.await?; println!("Completed! Amount: {}, Fee: {}", melted.amount(), melted.fee_paid()); } } // Option: Prefer async (returns Pending immediately if supported) let outcome = prepared.confirm_prefer_async().await?; // Handle same as above ``` ### New PreparedMelt Methods ```rust impl PreparedMelt { /// Confirm the melt (waits for immediate completion or returns Pending) async fn confirm(self) -> Result; /// Confirm with options (e.g., skip_swap) async fn confirm_with_options(self, options: MeltConfirmOptions) -> Result; /// Prefer async completion async fn confirm_prefer_async(self) -> Result; /// Cancel the prepared melt (releases reserved proofs) async fn cancel(self) -> Result<(), Error>; } ``` ### MeltConfirmOptions ```rust let options = MeltConfirmOptions::default() .skip_swap(true); // Skip swap before melt if needed let outcome = prepared.confirm_with_options(options).await?; ``` ### Handling Pending Melts ```rust use cdk::wallet::MeltOutcome; // Immediate await if let MeltOutcome::Pending(pending) = outcome { let melted = pending.await?; } // Or drop for background completion via WebSocket or finalize_pending_melts() drop(pending); // Will complete via notifications // Later: recover pending melts after restart wallet.finalize_pending_melts().await?; ``` ### Recovery ```rust // Recover incomplete sagas on startup wallet.recover_incomplete_sagas().await?; ``` ## FFI Changes FFI bindings follow the same pattern: ```swift // Before let wallet = MultiMintWallet(mnemonic: mnemonic, database: db) let quote = try await wallet.mintQuote(mintUrl: mintUrl, ...) // After let wallet = WalletRepository(mnemonic: mnemonic, database: db) try await wallet.addMint(mintUrl: mintUrl, unit: .sat, targetProofCount: 3) let mintWallet = try await wallet.getWallet(mintUrl: mintUrl, unit: .sat) let quote = try await mintWallet.mintQuote(...) ``` Same pattern applies to Kotlin and Python - `MultiMintWallet` → `WalletRepository`, get `Wallet` first, then call methods. ## Troubleshooting ### "no method named `mint_quote` found for struct `WalletRepository`" **Cause:** Calling wallet method on repository instead of getting wallet first. **Fix:** ```rust let mint_wallet = wallet.get_wallet(&mint_url, &CurrencyUnit::Sat).await?; let quote = mint_wallet.mint_quote(...).await?; ``` ### "expected struct `Amount`, found struct `BTreeMap`" **Cause:** `total_balance()` now returns map. **Fix:** ```rust let balances = wallet.total_balance().await?; let total = balances.get(&CurrencyUnit::Sat).copied().unwrap_or(Amount::ZERO); ``` ### "cannot find type `MultiMintWallet`" **Fix:** Update imports: ```rust use cdk::wallet::{WalletRepository, WalletRepositoryBuilder}; ``` ## References - [Changelog](../../CHANGELOG.md#0150) - [Issue #1577](https://github.com/cashubtc/cdk/issues/1577)