From 025c7fa9248d428da2f4c4bd873685a852e2bea0 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 15 Feb 2026 15:58:46 +0000 Subject: [PATCH 1/2] feat: update cdk to 0.15 --- app/build.gradle.kts | 2 +- .../numo/core/cashu/CashuWalletManager.kt | 62 ++++++++++--------- .../autowithdraw/AutoWithdrawManager.kt | 55 ++++++++-------- .../settings/WithdrawLightningActivity.kt | 10 ++- .../settings/WithdrawMeltQuoteActivity.kt | 27 ++++---- .../numo/ndef/CashuPaymentHelper.kt | 15 +++-- .../numo/payment/LightningMintHandler.kt | 46 ++++++++++---- .../payment/SwapToLightningMintManager.kt | 59 +++++++++++------- 8 files changed, 163 insertions(+), 113 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f18334c..f86b1954 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -125,7 +125,7 @@ dependencies { implementation("com.google.zxing:core:3.5.3") // CDK Kotlin bindings - implementation("org.cashudevkit:cdk-kotlin:0.14.4-rc.0") + implementation("org.cashudevkit:cdk-kotlin:0.15.0-rc.3") // ML Kit Barcode Scanning implementation("com.google.mlkit:barcode-scanning:17.3.0") diff --git a/app/src/main/java/com/electricdreams/numo/core/cashu/CashuWalletManager.kt b/app/src/main/java/com/electricdreams/numo/core/cashu/CashuWalletManager.kt index c2271f5c..48285892 100644 --- a/app/src/main/java/com/electricdreams/numo/core/cashu/CashuWalletManager.kt +++ b/app/src/main/java/com/electricdreams/numo/core/cashu/CashuWalletManager.kt @@ -10,16 +10,16 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.cashudevkit.CurrencyUnit import org.cashudevkit.MintUrl -import org.cashudevkit.MultiMintWallet import org.cashudevkit.Wallet import org.cashudevkit.WalletConfig import org.cashudevkit.WalletDatabaseImpl import org.cashudevkit.NoPointer import org.cashudevkit.WalletSqliteDatabase +import org.cashudevkit.WalletRepository import org.cashudevkit.generateMnemonic /** - * Global owner of the CDK MultiMintWallet and its backing SQLite database. + * Global owner of the CDK WalletRepository and its backing SQLite database. * * - Initialized from ModernPOSActivity.onCreate(). * - Re-initialized whenever the allowed mint list changes. @@ -40,7 +40,7 @@ object CashuWalletManager : MintManager.MintChangeListener { private var database: WalletSqliteDatabase? = null @Volatile - private var wallet: MultiMintWallet? = null + private var wallet: WalletRepository? = null /** Initialize from ModernPOSActivity. Safe to call multiple times. */ fun init(context: Context) { @@ -127,26 +127,22 @@ object CashuWalletManager : MintManager.MintChangeListener { val db = WalletSqliteDatabase(dbFile.absolutePath) // Create new wallet with restored mnemonic - val newWallet = MultiMintWallet( - CurrencyUnit.Sat, - newMnemonic, - db, - ) + val newWallet = WalletRepository(newMnemonic, db) // Add mints and restore each one - val targetProofCount: UInt = 10u for (mintUrl in mints) { try { onMintProgress(mintUrl, "Connecting...", balancesBefore[mintUrl] ?: 0L, 0L) - newWallet.addMint(MintUrl(mintUrl), targetProofCount) + newWallet.createWallet(MintUrl(mintUrl), CurrencyUnit.Sat, 10u) onMintProgress(mintUrl, "Restoring proofs...", balancesBefore[mintUrl] ?: 0L, 0L) // Use CDK's restore function to recover proofs - val recoveredAmount = newWallet.restore(MintUrl(mintUrl)) - val newBalance = recoveredAmount.value.toLong() + val mintWallet = newWallet.getWallet(MintUrl(mintUrl), CurrencyUnit.Sat) + val recoveredAmount = mintWallet?.restore()?.unspent?.value?.toLong() ?: 0L val oldBalance = balancesBefore[mintUrl] ?: 0L + val newBalance = recoveredAmount balanceChanges[mintUrl] = Pair(oldBalance, newBalance) @@ -170,7 +166,7 @@ object CashuWalletManager : MintManager.MintChangeListener { /** * Create a temporary, single-mint wallet instance for interacting with a - * mint that is not part of the main MultiMintWallet's allowed-mints set. + * mint that is not part of the main WalletRepository's allowed-mints set. * * This is used for swap-to-Lightning flows where we want to: * - keep our main wallet and balances untouched, and @@ -213,9 +209,9 @@ object CashuWalletManager : MintManager.MintChangeListener { } } - /** Current MultiMintWallet instance, or null if initialization failed or not complete. */ + /** Current WalletRepository instance, or null if initialization failed or not complete. */ @JvmStatic - fun getWallet(): MultiMintWallet? = wallet + fun getWallet(): WalletRepository? = wallet /** Current database instance, mostly for debugging or future use. */ fun getDatabase(): WalletSqliteDatabase? = database @@ -226,8 +222,15 @@ object CashuWalletManager : MintManager.MintChangeListener { suspend fun getBalanceForMint(mintUrl: String): Long { val w = wallet ?: return 0L return try { - val balanceMap = w.getBalances() - balanceMap[mintUrl]?.value?.toLong() ?: 0L + val balances = w.getBalances() + val normalizedInput = mintUrl.removeSuffix("/") + for (entry in balances) { + val cdkUrl = entry.key.mintUrl.url.removeSuffix("/") + if (cdkUrl == normalizedInput && entry.key.unit == CurrencyUnit.Sat) { + return entry.value.value.toLong() + } + } + 0L } catch (e: Exception) { Log.e(TAG, "Error getting balance for mint $mintUrl: ${e.message}", e) 0L @@ -242,7 +245,10 @@ object CashuWalletManager : MintManager.MintChangeListener { val w = wallet ?: return emptyMap() return try { val balanceMap = w.getBalances() - balanceMap.mapValues { (_, amount) -> amount.value.toLong() } + balanceMap + .filter { it.key.unit == CurrencyUnit.Sat } + .mapKeys { it.key.mintUrl.url.removeSuffix("/") } + .mapValues { it.value.value.toLong() } } catch (e: Exception) { Log.e(TAG, "Error getting mint balances: ${e.message}", e) emptyMap() @@ -256,7 +262,8 @@ object CashuWalletManager : MintManager.MintChangeListener { suspend fun fetchMintInfo(mintUrl: String): org.cashudevkit.MintInfo? { val w = wallet ?: return null return try { - w.fetchMintInfo(MintUrl(mintUrl)) + val mintWallet = w.getWallet(MintUrl(mintUrl), CurrencyUnit.Sat) + mintWallet?.fetchMintInfo() } catch (e: Exception) { Log.e(TAG, "Error fetching mint info for $mintUrl: ${e.message}", e) null @@ -416,18 +423,13 @@ object CashuWalletManager : MintManager.MintChangeListener { Log.i(TAG, "Loaded existing wallet mnemonic from preferences") } - // 3) Construct MultiMintWallet in sats. - val newWallet = MultiMintWallet( - CurrencyUnit.Sat, - mnemonic, - db, - ) + // 3) Construct WalletRepository in sats. + val newWallet = WalletRepository(mnemonic, db) - // 4) Register allowed mints with a default target proof count. - val targetProofCount: UInt = 10u + // 4) Register allowed mints. for (url in mints) { try { - newWallet.addMint(MintUrl(url), targetProofCount) + newWallet.createWallet(MintUrl(url), CurrencyUnit.Sat, 10u) } catch (t: Throwable) { Log.w(TAG, "Failed to add mint to wallet: ${'$'}url", t) } @@ -436,9 +438,9 @@ object CashuWalletManager : MintManager.MintChangeListener { database = db wallet = newWallet - Log.d(TAG, "Initialized MultiMintWallet with ${'$'}{mints.size} mints; DB=${'$'}{dbFile.absolutePath}") + Log.d(TAG, "Initialized WalletRepository with ${'$'}{mints.size} mints; DB=${'$'}{dbFile.absolutePath}") } catch (t: Throwable) { - Log.e(TAG, "Failed to initialize MultiMintWallet", t) + Log.e(TAG, "Failed to initialize WalletRepository", t) } } diff --git a/app/src/main/java/com/electricdreams/numo/feature/autowithdraw/AutoWithdrawManager.kt b/app/src/main/java/com/electricdreams/numo/feature/autowithdraw/AutoWithdrawManager.kt index 34414aef..8076b1ff 100644 --- a/app/src/main/java/com/electricdreams/numo/feature/autowithdraw/AutoWithdrawManager.kt +++ b/app/src/main/java/com/electricdreams/numo/feature/autowithdraw/AutoWithdrawManager.kt @@ -12,6 +12,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.cashudevkit.CurrencyUnit +import org.cashudevkit.FinalizedMelt import org.cashudevkit.MintUrl import org.cashudevkit.QuoteState import java.util.Date @@ -282,10 +284,14 @@ class AutoWithdrawManager private constructor(private val context: Context) { val amountMsat = withdrawAmount * 1000 Log.d(TAG, " Requesting quote for $withdrawAmount sats ($amountMsat msat) to $lightningAddress") + // Get the wallet for this mint first + val mintWallet = wallet.getWallet(MintUrl(mintUrl), CurrencyUnit.Sat) + ?: throw Exception("Failed to get wallet for mint: $mintUrl") + val meltQuote = withContext(Dispatchers.IO) { Log.d(TAG, " Making CDK call: wallet.meltLightningAddressQuote()") try { - val quote = wallet.meltLightningAddressQuote(MintUrl(mintUrl), lightningAddress, amountMsat.toULong()) + val quote = mintWallet.meltLightningAddressQuote(lightningAddress, org.cashudevkit.Amount(amountMsat.toULong())) Log.d(TAG, " ✅ Quote received: id=${quote.id}") quote } catch (e: Exception) { @@ -318,52 +324,46 @@ class AutoWithdrawManager private constructor(private val context: Context) { feeSats = feeReserve ) - // Execute melt + // Execute melt using simplified API Log.d(TAG, "📋 Step 4: Executing melt operation...") withContext(Dispatchers.Main) { progressListener?.onWithdrawProgress("Sending", "Sending payment...") } - val melted = withContext(Dispatchers.IO) { - Log.d(TAG, " Making CDK call: wallet.meltWithMint()") + val finalized: FinalizedMelt = withContext(Dispatchers.IO) { + Log.d(TAG, " Making CDK call: wallet.prepareMelt() + confirm()") try { - val result = wallet.meltWithMint(MintUrl(mintUrl), meltQuote.id) - Log.d(TAG, " ✅ Melt completed") + val prepared = mintWallet.prepareMelt(meltQuote.id) + val result = prepared.confirm() + Log.d(TAG, " Melt confirm returned: state=${result.state}, feePaid=${result.feePaid.value}, preimage=${result.preimage != null}") result } catch (e: Exception) { - Log.e(TAG, " ❌ Melt failed: ${e.message}", e) + Log.e(TAG, " Melt failed: ${e.message}", e) throw e } } // Check melt state - Log.d(TAG, "📋 Step 5: Checking final quote state...") - val finalQuote = withContext(Dispatchers.IO) { - Log.d(TAG, " Making CDK call: wallet.checkMeltQuote()") - try { - val quote = wallet.checkMeltQuote(MintUrl(mintUrl), meltQuote.id) - Log.d(TAG, " ✅ Final quote state: ${quote.state}") - quote - } catch (e: Exception) { - Log.e(TAG, " ❌ Quote check failed: ${e.message}", e) - throw e - } - } + Log.d(TAG, "📋 Step 5: Checking melt result state...") + val actualFee = finalized.feePaid.value.toLong() - when (finalQuote.state) { + when (finalized.state) { QuoteState.PAID -> { Log.d(TAG, "🎉 AUTO-WITHDRAWAL SUCCESSFUL!") Log.d(TAG, " Amount withdrawn: $withdrawAmount sats") - Log.d(TAG, " Fee paid: $feeReserve sats") + Log.d(TAG, " Fee paid: $actualFee sats (reserved: $feeReserve)") Log.d(TAG, " Lightning address: $lightningAddress") - historyEntry = historyEntry.copy(status = WithdrawHistoryEntry.STATUS_COMPLETED) + historyEntry = historyEntry.copy( + status = WithdrawHistoryEntry.STATUS_COMPLETED, + feeSats = actualFee + ) // Broadcast balance change so other activities can refresh BalanceRefreshBroadcast.send(context, BalanceRefreshBroadcast.REASON_AUTO_WITHDRAWAL) withContext(Dispatchers.Main) { - progressListener?.onWithdrawCompleted(mintUrl, withdrawAmount, feeReserve) + progressListener?.onWithdrawCompleted(mintUrl, withdrawAmount, actualFee) } } QuoteState.PENDING -> { @@ -381,8 +381,8 @@ class AutoWithdrawManager private constructor(private val context: Context) { throw Exception("Payment failed: Quote state is UNPAID") } else -> { - Log.e(TAG, "❌ Auto-withdrawal failed: Unknown quote state ${finalQuote.state}") - throw Exception("Payment failed: Unknown quote state ${finalQuote.state}") + Log.e(TAG, "❌ Auto-withdrawal failed: Unknown quote state ${finalized.state}") + throw Exception("Payment failed: Unknown quote state ${finalized.state}") } } @@ -493,14 +493,15 @@ class AutoWithdrawManager private constructor(private val context: Context) { /** * Update the status (and optional error message) of a withdrawal entry. */ - fun updateWithdrawalStatus(id: String, status: String, errorMessage: String? = null) { + fun updateWithdrawalStatus(id: String, status: String, errorMessage: String? = null, feeSats: Long? = null) { val history = getHistory().toMutableList() val index = history.indexOfFirst { it.id == id } if (index >= 0) { val existing = history[index] val updated = existing.copy( status = status, - errorMessage = errorMessage ?: existing.errorMessage + errorMessage = errorMessage ?: existing.errorMessage, + feeSats = feeSats ?: existing.feeSats ) history[index] = updated saveHistory(history) diff --git a/app/src/main/java/com/electricdreams/numo/feature/settings/WithdrawLightningActivity.kt b/app/src/main/java/com/electricdreams/numo/feature/settings/WithdrawLightningActivity.kt index 1118ceb9..dab8742e 100644 --- a/app/src/main/java/com/electricdreams/numo/feature/settings/WithdrawLightningActivity.kt +++ b/app/src/main/java/com/electricdreams/numo/feature/settings/WithdrawLightningActivity.kt @@ -25,6 +25,7 @@ import com.google.android.material.card.MaterialCardView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.cashudevkit.CurrencyUnit import org.cashudevkit.MintUrl /** @@ -199,8 +200,11 @@ class WithdrawLightningActivity : AppCompatActivity() { } // Get melt quote + val meltQuoteObj = MintUrl(mintUrl) + val mintWallet = wallet.getWallet(meltQuoteObj, CurrencyUnit.Sat) + ?: throw Exception("Failed to get wallet for mint: $mintUrl") val meltQuote = withContext(Dispatchers.IO) { - wallet.meltQuote(MintUrl(mintUrl), invoice, null) + mintWallet.meltQuote(org.cashudevkit.PaymentMethod.Bolt11, invoice, null,null) } withContext(Dispatchers.Main) { @@ -273,8 +277,10 @@ class WithdrawLightningActivity : AppCompatActivity() { // Get melt quote for Lightning address val amountMsat = amountSats * 1000 + val mintWallet = wallet.getWallet(MintUrl(mintUrl), CurrencyUnit.Sat) + ?: throw Exception("Failed to get wallet for mint: $mintUrl") val meltQuote = withContext(Dispatchers.IO) { - wallet.meltLightningAddressQuote(MintUrl(mintUrl), address, amountMsat.toULong()) + mintWallet.meltLightningAddressQuote(address, org.cashudevkit.Amount(amountMsat.toULong())) } withContext(Dispatchers.Main) { diff --git a/app/src/main/java/com/electricdreams/numo/feature/settings/WithdrawMeltQuoteActivity.kt b/app/src/main/java/com/electricdreams/numo/feature/settings/WithdrawMeltQuoteActivity.kt index 4d58e667..1d4d6f8d 100644 --- a/app/src/main/java/com/electricdreams/numo/feature/settings/WithdrawMeltQuoteActivity.kt +++ b/app/src/main/java/com/electricdreams/numo/feature/settings/WithdrawMeltQuoteActivity.kt @@ -18,6 +18,9 @@ import com.electricdreams.numo.feature.autowithdraw.AutoWithdrawManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.cashudevkit.CurrencyUnit +import org.cashudevkit.FinalizedMelt +import org.cashudevkit.MintUrl import org.cashudevkit.QuoteState class WithdrawMeltQuoteActivity : AppCompatActivity() { @@ -179,32 +182,34 @@ class WithdrawMeltQuoteActivity : AppCompatActivity() { updateProcessingState(ProcessingStep.CONTACTING) } - val melted = withContext(Dispatchers.IO) { - wallet.meltWithMint(org.cashudevkit.MintUrl(mintUrl), quoteId) - } - - val state = melted.state - Log.d(TAG, "Melt state after melt: $state") + // Get the wallet for this mint + val mintWallet = wallet.getWallet(MintUrl(mintUrl), CurrencyUnit.Sat) + ?: throw Exception("Failed to get wallet for mint: $mintUrl") - val meltQuote = withContext(Dispatchers.IO) { - wallet.checkMeltQuote(org.cashudevkit.MintUrl(mintUrl), quoteId) + // Prepare and confirm melt + val finalized: FinalizedMelt = withContext(Dispatchers.IO) { + val prepared = mintWallet.prepareMelt(quoteId) + prepared.confirm() } - Log.d(TAG, "Melt state after check: ${meltQuote.state}") + Log.d(TAG, "Melt completed: state=${finalized.state}, feePaid=${finalized.feePaid.value}, preimage=${finalized.preimage != null}") withContext(Dispatchers.Main) { updateProcessingState(ProcessingStep.SETTLING) } + val actualFee = finalized.feePaid.value.toLong() + withContext(Dispatchers.Main) { setLoading(false) - when (meltQuote.state) { + when (finalized.state) { QuoteState.PAID -> { withdrawEntryId?.let { autoWithdrawManager.updateWithdrawalStatus( id = it, - status = com.electricdreams.numo.feature.autowithdraw.WithdrawHistoryEntry.STATUS_COMPLETED + status = com.electricdreams.numo.feature.autowithdraw.WithdrawHistoryEntry.STATUS_COMPLETED, + feeSats = actualFee ) } showPaymentSuccess() diff --git a/app/src/main/java/com/electricdreams/numo/ndef/CashuPaymentHelper.kt b/app/src/main/java/com/electricdreams/numo/ndef/CashuPaymentHelper.kt index 9f70436b..7e6ea268 100644 --- a/app/src/main/java/com/electricdreams/numo/ndef/CashuPaymentHelper.kt +++ b/app/src/main/java/com/electricdreams/numo/ndef/CashuPaymentHelper.kt @@ -16,7 +16,6 @@ import com.google.gson.* import kotlinx.coroutines.runBlocking import org.cashudevkit.CurrencyUnit import org.cashudevkit.MintUrl -import org.cashudevkit.MultiMintReceiveOptions import org.cashudevkit.ReceiveOptions import org.cashudevkit.SplitTarget import org.cashudevkit.Token as CdkToken @@ -348,6 +347,11 @@ object CashuPaymentHelper { val mintUrl: MintUrl = cdkToken.mintUrl() + // Receive into wallet - getWallet is suspend so use runBlocking + val mintWallet = runBlocking { + wallet.getWallet(mintUrl, CurrencyUnit.Sat) + } ?: throw RedemptionException("Failed to get wallet for mint: ${mintUrl.url}") + // Receive into wallet val receiveOptions = ReceiveOptions( amountSplitTarget = SplitTarget.None, @@ -355,15 +359,10 @@ object CashuPaymentHelper { preimages = emptyList(), metadata = emptyMap(), ) - val mmReceive = MultiMintReceiveOptions( - allowUntrusted = false, - transferToMint = null, - receiveOptions = receiveOptions, - ) // Receive into wallet runBlocking { - wallet.receive(cdkToken, mmReceive) + mintWallet.receive(cdkToken, receiveOptions) } Log.d(TAG, "Token received via CDK successfully (mintUrl=${mintUrl.url})") @@ -385,7 +384,7 @@ object CashuPaymentHelper { * * Behavior: * - Validates the token structure and amount against expectedAmount. - * - If mint is in allowedMints → normal Cashu redemption via MultiMintWallet. + * - If mint is in allowedMints → normal Cashu redemption via WalletRepository. * - If mint is *not* in allowedMints but amount is sufficient → * runs the SwapToLightningMint flow and treats Lightning receipt as * payment success (returns empty string as token, Lightning-style). diff --git a/app/src/main/java/com/electricdreams/numo/payment/LightningMintHandler.kt b/app/src/main/java/com/electricdreams/numo/payment/LightningMintHandler.kt index 82d8e93c..8cb1622d 100644 --- a/app/src/main/java/com/electricdreams/numo/payment/LightningMintHandler.kt +++ b/app/src/main/java/com/electricdreams/numo/payment/LightningMintHandler.kt @@ -18,8 +18,11 @@ import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener import org.cashudevkit.Amount as CdkAmount +import org.cashudevkit.CurrencyUnit import org.cashudevkit.MintQuote import org.cashudevkit.MintUrl +import org.cashudevkit.PaymentMethod +import org.cashudevkit.QuoteState import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean @@ -101,7 +104,7 @@ class LightningMintHandler( fun start(paymentAmount: Long, callback: Callback) { val wallet = CashuWalletManager.getWallet() if (wallet == null) { - Log.w(TAG, "MultiMintWallet not ready, skipping Lightning") + Log.w(TAG, "WalletRepository not ready, skipping Lightning") callback.onError("Wallet not ready") return } @@ -145,7 +148,9 @@ class LightningMintHandler( val quoteAmount = CdkAmount(paymentAmount.toULong()) Log.d(TAG, "Requesting Lightning mint quote from ${mintUrl.url} for $paymentAmount sats") - val quote = wallet.mintQuote(mintUrl, quoteAmount, "Numo POS payment of $paymentAmount sats") + val mintWallet = wallet.getWallet(mintUrl, CurrencyUnit.Sat) + val quote = mintWallet?.mintQuote(org.cashudevkit.PaymentMethod.Bolt11, quoteAmount, "Numo POS payment of $paymentAmount sats", null) + ?: throw Exception("Failed to get wallet for mint: ${mintUrl.url}") mintQuote = quote val bolt11 = quote.request @@ -478,7 +483,15 @@ class LightningMintHandler( } Log.d(TAG, "Mint quote $quoteId is paid (detected by $source), calling wallet.mint") - val proofs = wallet.mint(mintUrl, quoteId, null) + val mintWallet = wallet.getWallet(mintUrl, CurrencyUnit.Sat) + val proofs = mintWallet?.mint(quoteId, org.cashudevkit.SplitTarget.None, null) + ?: run { + Log.e(TAG, "Failed to get wallet for mint: ${mintUrl.url}") + uiScope.launch(Dispatchers.Main) { + callback.onError("Wallet not ready") + } + return false + } Log.d(TAG, "Lightning mint completed with ${proofs.size} proofs ($source)") uiScope.launch(Dispatchers.Main) { @@ -515,16 +528,27 @@ class LightningMintHandler( } Log.v(TAG, "Polling mint quote state for $quoteId") - val quote = wallet.checkMintQuote(mintUrl, quoteId) - val stateStr = quote.state.toString() - Log.d(TAG, "Poll result for $quoteId: state=$stateStr") + // Check quote state using checkMintQuote API + val mintWallet = wallet.getWallet(mintUrl, CurrencyUnit.Sat) + ?: throw Exception("Failed to get wallet for mint: ${mintUrl.url}") - // Compare state as string (consistent with WebSocket handling) - if (stateStr.equals("PAID", ignoreCase = true) || - stateStr.equals("ISSUED", ignoreCase = true)) { - tryMintOnce(mintUrl, quoteId, callback, "polling") - break + val quote = mintWallet.checkMintQuote( quoteId) + + when (quote.state) { + QuoteState.PAID, QuoteState.ISSUED -> { + Log.d(TAG, "Quote $quoteId is ${quote.state} (detected via polling)") + tryMintOnce(mintUrl, quoteId, callback, "polling") + break + } + QuoteState.UNPAID -> { + Log.v(TAG, "Quote $quoteId still UNPAID, continuing poll") + // Continue polling + } + else -> { + Log.w(TAG, "Quote $quoteId in unexpected state: ${quote.state}") + // Continue polling for other states + } } } catch (ce: CancellationException) { Log.d(TAG, "Polling cancelled for quote $quoteId") diff --git a/app/src/main/java/com/electricdreams/numo/payment/SwapToLightningMintManager.kt b/app/src/main/java/com/electricdreams/numo/payment/SwapToLightningMintManager.kt index e2b9da29..1300c9df 100644 --- a/app/src/main/java/com/electricdreams/numo/payment/SwapToLightningMintManager.kt +++ b/app/src/main/java/com/electricdreams/numo/payment/SwapToLightningMintManager.kt @@ -8,9 +8,11 @@ import com.electricdreams.numo.nostr.Bech32 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.cashudevkit.Amount as CdkAmount +import org.cashudevkit.CurrencyUnit +import org.cashudevkit.FinalizedMelt import org.cashudevkit.MeltQuote -import org.cashudevkit.Melted import org.cashudevkit.MintUrl +import org.cashudevkit.QuoteState import java.security.MessageDigest import kotlin.math.roundToLong import com.electricdreams.numo.feature.history.PaymentsHistoryActivity @@ -158,7 +160,13 @@ object SwapToLightningMintManager { "lightningMintUrl=$lightningMintUrl, amount=$lightningAmount" ) - val mintQuote = wallet.mintQuote(MintUrl(lightningMintUrl), CdkAmount(lightningAmount.toULong()), null) + val lightningMintUrlObj = MintUrl(lightningMintUrl) + val lightningWallet = wallet.getWallet(lightningMintUrlObj, CurrencyUnit.Sat) + ?: run { + Log.e(TAG, "Failed to get Lightning wallet for: $lightningMintUrl") + return@withContext SwapResult.Failure("Failed to get Lightning wallet") + } + val mintQuote = lightningWallet.mintQuote(org.cashudevkit.PaymentMethod.Bolt11, CdkAmount(lightningAmount.toULong()), null, null) Log.d( TAG, @@ -168,7 +176,7 @@ object SwapToLightningMintManager { var meltQuote: MeltQuote = try { Log.d(TAG, "swapFromUnknownMint: requesting initial melt quote from unknown mint for preliminary Lightning invoice") - tempWallet.meltQuote(mintQuote.request, null) + tempWallet.meltQuote(org.cashudevkit.PaymentMethod.Bolt11, mintQuote.request, null, null) } catch (t: Throwable) { val msg = "Failed to request melt quote from unknown mint: ${'$'}{t.message}" Log.e(TAG, msg, t) @@ -204,7 +212,7 @@ object SwapToLightningMintManager { "lightningMintUrl=$lightningMintUrl, mintQuoteAmount=$lightningAmount" ) - val finalMintQuote = wallet.mintQuote(MintUrl(lightningMintUrl), CdkAmount(lightningAmount.toULong()), null) + val finalMintQuote = lightningWallet.mintQuote(org.cashudevkit.PaymentMethod.Bolt11, CdkAmount(lightningAmount.toULong()), null, null) val bolt11 = finalMintQuote.request Log.d( @@ -216,7 +224,7 @@ object SwapToLightningMintManager { // 4) Request a melt quote from the unknown mint for this bolt11. meltQuote = try { Log.d(TAG, "swapFromUnknownMint: requesting final melt quote from unknown mint using Lightning bolt11 invoice") - tempWallet.meltQuote(bolt11, null) + tempWallet.meltQuote(org.cashudevkit.PaymentMethod.Bolt11, bolt11, null, null) } catch (t: Throwable) { val msg = "Failed to request melt quote from unknown mint: ${'$'}{t.message}" Log.e(TAG, msg, t) @@ -279,15 +287,15 @@ object SwapToLightningMintManager { } // 5) Execute melt on unknown mint using the temporary single-mint wallet. - // The Melted result carries the final state, preimage, and fee info - // for this Lightning payment. - val melted: Melted = try { + val finalized: FinalizedMelt = try { Log.d( TAG, "swapFromUnknownMint: executing melt on unknown mint: " + "meltQuoteId=${meltQuote.id}, proofsCount=${proofs.size}, totalMeltRequired=$totalMeltRequired" ) - tempWallet.meltProofs(meltQuote.id, proofs) + + val prepared = tempWallet.prepareMelt(meltQuote.id) + prepared.confirm() } catch (t: Throwable) { val msg = "Melt execution failed on unknown mint: ${t.message}" Log.e(TAG, msg, t) @@ -299,11 +307,11 @@ object SwapToLightningMintManager { } } - Log.d(TAG, "swapFromUnknownMint: melt result state=${melted.state}, preimagePresent=${melted.preimage != null}") + Log.d(TAG, "swapFromUnknownMint: melt result: state=${finalized.state}, feePaid=${finalized.feePaid.value}, preimage=${finalized.preimage}") - if (melted.state != org.cashudevkit.QuoteState.PAID) { - val msg = "Unknown-mint melt did not complete: state=${melted.state}" - Log.w(TAG, msg) + if (finalized.state != QuoteState.PAID) { + val msg = "Melt not paid on unknown mint: state=${finalized.state}" + Log.e(TAG, msg) return@withContext SwapResult.Failure(msg) } @@ -319,18 +327,23 @@ object SwapToLightningMintManager { // (e.g. LightningMintHandler) may also mint the quote if they // were started from a separate Lightning-receive UI flow. try { - val lightningMint = MintUrl(lightningMintUrl) - Log.d(TAG, "Checking Lightning mint quote state for quoteId=${finalMintQuote.id}") - val lightningQuote = wallet.checkMintQuote(lightningMint, finalMintQuote.id) - val state = lightningQuote.state - Log.d(TAG, "Lightning mint quote state=$state") + // Use checkMintQuote to verify quote is paid before minting + val checkedQuote = try { + lightningWallet.checkMintQuote(finalMintQuote.id) + } catch (checkError: Throwable) { + val msg = "Failed to check Lightning mint quote state for quoteId=${finalMintQuote.id}: ${checkError.message}" + Log.e(TAG, msg, checkError) + return@withContext SwapResult.Failure(msg) + } + + Log.d(TAG, "Lightning mint quote state=${checkedQuote.state}") - when (state) { - org.cashudevkit.QuoteState.PAID -> { + when (checkedQuote.state) { + QuoteState.PAID -> { Log.d(TAG, "Lightning quote is PAID; attempting wallet.mint for quoteId=${finalMintQuote.id}") val mintedProofs = try { - wallet.mint(lightningMint, finalMintQuote.id, null) + lightningWallet.mint(finalMintQuote.id, org.cashudevkit.SplitTarget.None, null) } catch (mintError: Throwable) { val msg = "Failed to mint proofs on Lightning mint for quoteId=${finalMintQuote.id}: ${mintError.message}" Log.e(TAG, msg, mintError) @@ -345,13 +358,13 @@ object SwapToLightningMintManager { Log.d(TAG, "Minted ${mintedProofs.size} proofs on Lightning mint as part of swap flow") } - org.cashudevkit.QuoteState.ISSUED -> { + QuoteState.ISSUED -> { // Quote already issued/minted by another component (e.g. LightningMintHandler). // We consider this a success condition for the swap. Log.d(TAG, "Lightning mint quote already ISSUED; assuming proofs are available for quoteId=${finalMintQuote.id}") } else -> { - val msg = "Lightning mint quote not paid after unknown-mint melt (state=$state)" + val msg = "Lightning mint quote not paid after unknown-mint melt (state=${checkedQuote.state})" Log.w(TAG, msg) return@withContext SwapResult.Failure(msg) } From 569a4e19506050b7c827e20821721905e8b47189 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 16 Feb 2026 19:48:08 +0000 Subject: [PATCH 2/2] fix: tests with cdk 0.15 --- .../autowithdraw/AutoWithdrawManagerTest.kt | 4 +-- .../numo/payment/LightningMintHandlerTest.kt | 28 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/app/src/test/java/com/electricdreams/numo/feature/autowithdraw/AutoWithdrawManagerTest.kt b/app/src/test/java/com/electricdreams/numo/feature/autowithdraw/AutoWithdrawManagerTest.kt index 7e536885..7363fa49 100644 --- a/app/src/test/java/com/electricdreams/numo/feature/autowithdraw/AutoWithdrawManagerTest.kt +++ b/app/src/test/java/com/electricdreams/numo/feature/autowithdraw/AutoWithdrawManagerTest.kt @@ -7,7 +7,7 @@ import com.electricdreams.numo.core.util.MintManager import kotlinx.coroutines.test.runTest import org.cashudevkit.Amount import org.cashudevkit.CurrencyUnit -import org.cashudevkit.MultiMintWallet +import org.cashudevkit.WalletRepository import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -35,7 +35,7 @@ class AutoWithdrawManagerTest { private lateinit var mockMintManager: MintManager // We also need to control CashuWalletManager singleton - private lateinit var mockWallet: MultiMintWallet + private lateinit var mockWallet: WalletRepository @Before fun setUp() { diff --git a/app/src/test/java/com/electricdreams/numo/payment/LightningMintHandlerTest.kt b/app/src/test/java/com/electricdreams/numo/payment/LightningMintHandlerTest.kt index 42bbcb2e..b56791bd 100644 --- a/app/src/test/java/com/electricdreams/numo/payment/LightningMintHandlerTest.kt +++ b/app/src/test/java/com/electricdreams/numo/payment/LightningMintHandlerTest.kt @@ -2,7 +2,8 @@ package com.electricdreams.numo.payment import android.util.Log import com.electricdreams.numo.core.cashu.CashuWalletManager -import org.cashudevkit.MultiMintWallet +import org.cashudevkit.WalletRepository +import org.cashudevkit.Wallet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -54,7 +55,9 @@ import java.util.concurrent.atomic.AtomicBoolean class LightningMintHandlerTest { @Mock - private lateinit var mockWallet: MultiMintWallet + private lateinit var mockWalletRepository: WalletRepository + @Mock + private lateinit var mockWallet: Wallet @Mock private lateinit var mockCallback: LightningMintHandler.Callback @Mock @@ -90,7 +93,12 @@ class LightningMintHandlerTest { // Mock static CashuWalletManager cashuWalletManagerMock = mockStatic(CashuWalletManager::class.java) - cashuWalletManagerMock.`when` { CashuWalletManager.getWallet() }.thenReturn(mockWallet) + cashuWalletManagerMock.`when` { CashuWalletManager.getWallet() }.thenReturn(mockWalletRepository) + + // Mock WalletRepository.getWallet() to return our mock Wallet (it's a suspend function) + runBlocking { + `when`(mockWalletRepository.getWallet(org.mockito.kotlin.any(), org.mockito.kotlin.any())).thenReturn(mockWallet) + } // Setup handler with dynamic server url and injected dispatcher handler = LightningMintHandler(useUrl, listOf(useUrl), testScope, UnconfinedTestDispatcher()) @@ -117,7 +125,7 @@ class LightningMintHandlerTest { // Unset wallet cashuWalletManagerMock.close() cashuWalletManagerMock = mockStatic(CashuWalletManager::class.java) - cashuWalletManagerMock.`when` { CashuWalletManager.getWallet() }.thenReturn(null) + cashuWalletManagerMock.`when` { CashuWalletManager.getWallet() }.thenReturn(null) // Re-setup logs if needed since we closed mocks @@ -125,7 +133,7 @@ class LightningMintHandlerTest { // Then verify(mockCallback).onError("Wallet not ready") - verify(mockWallet, never()).mintQuote(any(), any(), any()) + verify(mockWallet, never()).mintQuote(any(), any(), any(), any()) } } @@ -148,10 +156,10 @@ class LightningMintHandlerTest { runBlocking { // Re-assert that wallet IS ready (from setup), so we pass the first check - cashuWalletManagerMock.`when` { CashuWalletManager.getWallet() }.thenReturn(mockWallet) + cashuWalletManagerMock.`when` { CashuWalletManager.getWallet() }.thenReturn(mockWalletRepository) // Stub wallet to throw if called, avoiding NPE and verifying error propagation - `when`(mockWallet.mintQuote(any(), any(), any())).thenThrow(RuntimeException("Wallet rejected URL")) + `when`(mockWallet.mintQuote(any(), any(), any(), any())).thenThrow(RuntimeException("Wallet rejected URL")) // Given invalid mint URL val invalidHandler = LightningMintHandler("http://exa mple.com", listOf("http://exa mple.com"), testScope, UnconfinedTestDispatcher()) @@ -177,10 +185,10 @@ class LightningMintHandlerTest { val handler = LightningMintHandler(mintUrlStr, listOf(mintUrlStr), testScope, testDispatcher) // We stub mintQuote to return successfully - doReturn(mockMintQuote).`when`(mockWallet).mintQuote(anyOrNull(), anyOrNull(), anyOrNull()) + doReturn(mockMintQuote).`when`(mockWallet).mintQuote(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) // First check returns unpaid - doReturn(mockMintQuote).`when`(mockWallet).checkMintQuote(org.mockito.kotlin.any(), org.mockito.kotlin.any()) + doReturn(mockMintQuote).`when`(mockWallet).checkMintQuote(org.mockito.kotlin.any()) // Start process handler.start(paymentAmount, mockCallback) @@ -195,7 +203,7 @@ class LightningMintHandlerTest { // Wait - we need to ensure the coroutines that are polling get to run // We'll update the mock to return PAID for checkMintQuote - doReturn(paidQuote).`when`(mockWallet).checkMintQuote(org.mockito.kotlin.any(), org.mockito.kotlin.any()) + doReturn(paidQuote).`when`(mockWallet).checkMintQuote(org.mockito.kotlin.any()) // And return proofs for mint doReturn(listOf()).`when`(mockWallet).mint(org.mockito.kotlin.any(), org.mockito.kotlin.any(), org.mockito.kotlin.anyOrNull())