v0.15.md 11 KB

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 patternconfirm() 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<MintUrl, Amount> BTreeMap<WalletKey, Amount>
Total Amount BTreeMap<CurrencyUnit, Amount>

Detailed Changes

1. Construction

// Before
let wallet = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat).await?;

// After
let wallet = WalletRepositoryBuilder::new()
    .localstore(localstore)
    .seed(seed)
    .build()
    .await?;

2. Adding Mints

// 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:

// 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:

// Before
let proofs = wallet
    .wait_for_mint_quote(&mint_url, &quote_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

// 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);
// Before - HashMap<MintUrl, Amount>
let balances: HashMap<MintUrl, Amount> = wallet.get_balances().await?;
for (mint_url, balance) in balances {
    println!("{}: {}", mint_url, balance);
}

// After - BTreeMap<WalletKey, Amount> (includes unit)
let balances: BTreeMap<WalletKey, Amount> = wallet.get_balances().await?;
for (key, balance) in balances {
    println!("{} ({}): {}", key.mint_url, key.unit, balance);
}

5. Receiving Tokens

// 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

impl WalletRepository {
    // Get specific wallet
    async fn get_wallet(&self, mint_url: &MintUrl, unit: &CurrencyUnit) -> Result<Wallet>;
    
    // Get all wallets for a mint
    async fn get_wallets_for_mint(&self, mint_url: &MintUrl) -> Vec<Wallet>;
    
    // 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<WalletConfig>) -> Result<Wallet>;
    async fn set_mint_config(&self, mint_url: MintUrl, unit: CurrencyUnit, config: WalletConfig) -> Result<Wallet>;
    
    // Remove
    async fn remove_wallet(&self, mint_url: MintUrl, unit: CurrencyUnit) -> Result<()>;
    
    // Helpers
    async fn get_token_data(&self, token: &Token) -> Result<TokenData>;
    async fn check_all_mint_quotes(&self, mint_url: Option<MintUrl>) -> Result<Amount>;
}

Type Changes

Old New
MultiMintWallet WalletRepository
MultiMintReceiveOptions ReceiveOptions
HashMap<MintUrl, Amount> (balances) BTreeMap<WalletKey, Amount>
Amount (total_balance) BTreeMap<CurrencyUnit, Amount>

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

// Before: prepare_melt returned a result that completed immediately
let prepared = wallet.prepare_melt(&quote_id, HashMap::new()).await?;
let result = prepared.confirm().await?;
// 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

impl PreparedMelt {
    /// Confirm the melt (waits for immediate completion or returns Pending)
    async fn confirm(self) -> Result<MeltOutcome, Error>;
    
    /// Confirm with options (e.g., skip_swap)
    async fn confirm_with_options(self, options: MeltConfirmOptions) -> Result<MeltOutcome, Error>;
    
    /// Prefer async completion
    async fn confirm_prefer_async(self) -> Result<MeltOutcome, Error>;
    
    /// Cancel the prepared melt (releases reserved proofs)
    async fn cancel(self) -> Result<(), Error>;
}

MeltConfirmOptions

let options = MeltConfirmOptions::default()
    .skip_swap(true); // Skip swap before melt if needed

let outcome = prepared.confirm_with_options(options).await?;

Handling Pending Melts

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

// Recover incomplete sagas on startup
wallet.recover_incomplete_sagas().await?;

FFI Changes

FFI bindings follow the same pattern:

// 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 - MultiMintWalletWalletRepository, 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:

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:

let balances = wallet.total_balance().await?;
let total = balances.get(&CurrencyUnit::Sat).copied().unwrap_or(Amount::ZERO);

"cannot find type MultiMintWallet"

Fix: Update imports:

use cdk::wallet::{WalletRepository, WalletRepositoryBuilder};

References