Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 -> {
Expand All @@ -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}")
}
}

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
Expand Down
Loading