This commit is contained in:
callebtc
2025-07-15 00:42:27 +02:00
parent 618bff972d
commit 2bdb8623c7
4 changed files with 234 additions and 537 deletions
+3
View File
@@ -19,6 +19,9 @@
<!-- Haptic feedback permission --> <!-- Haptic feedback permission -->
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<!-- Internet permission -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Hardware features --> <!-- Hardware features -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true" /> <uses-feature android:name="android.hardware.bluetooth" android:required="true" />
@@ -3,14 +3,14 @@ package com.bitchat.android.wallet.service
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import android.util.Log import android.util.Log
import android.content.Context
import com.bitchat.android.wallet.data.* import com.bitchat.android.wallet.data.*
import uniffi.cdk_ffi.* import uniffi.cdk_ffi.*
import java.math.BigDecimal import java.math.BigDecimal
import java.util.* import java.util.*
/** /**
* Real Cashu service using the CDK FFI library with graceful fallback * Real Cashu service using the CDK FFI library
* when CDK is not available on the device architecture
*/ */
class CashuService { class CashuService {
@@ -20,11 +20,10 @@ class CashuService {
private var currentUnit: String = "sat" private var currentUnit: String = "sat"
private var isInitialized = false private var isInitialized = false
private var isCdkAvailable = false private var isCdkAvailable = false
private var mockBalance = 1000L // Fallback balance
companion object { companion object {
private const val TAG = "CashuService" private const val TAG = "CashuService"
private const val DEFAULT_MINT_URL = "https://mint.minibits.cash/Bitcoin" private const val DEFAULT_MINT_URL = "https://testnut.cashu.space"
@Volatile @Volatile
private var INSTANCE: CashuService? = null private var INSTANCE: CashuService? = null
@@ -44,151 +43,26 @@ class CashuService {
Log.d(TAG, "=== CDK LIBRARY AVAILABILITY CHECK ===") Log.d(TAG, "=== CDK LIBRARY AVAILABILITY CHECK ===")
// Check device architecture
val arch = System.getProperty("os.arch")
val arch64 = System.getProperty("os.arch").contains("64")
val abiList = android.os.Build.SUPPORTED_64_BIT_ABIS.joinToString(", ")
val primaryAbi = android.os.Build.SUPPORTED_ABIS[0]
Log.d(TAG, "Device Architecture Info:")
Log.d(TAG, " - os.arch: $arch")
Log.d(TAG, " - 64-bit: $arch64")
Log.d(TAG, " - Primary ABI: $primaryAbi")
Log.d(TAG, " - Supported 64-bit ABIs: $abiList")
Log.d(TAG, " - All ABIs: ${android.os.Build.SUPPORTED_ABIS.joinToString(", ")}")
// Check JNA availability
try { try {
val jnaVersion = com.sun.jna.Native.VERSION // Test CDK availability by calling a simple function
val jnaNativeVersion = com.sun.jna.Native.VERSION_NATIVE
Log.d(TAG, "JNA Library Info:")
Log.d(TAG, " - JNA Version: $jnaVersion")
Log.d(TAG, " - JNA Native Version: $jnaNativeVersion")
Log.d(TAG, " - JNA Platform: ${System.getProperty("jna.platform.library.path")}")
Log.d(TAG, " - JNA Library Path: ${System.getProperty("jna.library.path")}")
} catch (e: Exception) {
Log.w(TAG, "JNA Library issue: ${e.message}")
}
// Check native library paths
val libraryPath = System.getProperty("java.library.path")
Log.d(TAG, "Java Library Path: $libraryPath")
// Check if CDK classes are available
Log.d(TAG, "Checking CDK-FFI Classes:")
val cdkClasses = listOf(
"uniffi.cdk_ffi.FfiWallet",
"uniffi.cdk_ffi.FfiLocalStore",
"uniffi.cdk_ffi.FfiAmount",
"uniffi.cdk_ffi.FfiException",
"uniffi.cdk_ffi.Cdk_ffiKt"
)
for (className in cdkClasses) {
try {
Class.forName(className)
Log.d(TAG, "$className - Found")
} catch (e: ClassNotFoundException) {
Log.w(TAG, "$className - Not found")
}
}
// Check native library loading
Log.d(TAG, "Checking Native Library Loading:")
val libraryNames = listOf("cdk_ffi", "libcdk_ffi", "cdk_ffi.so", "libcdk_ffi.so")
for (libName in libraryNames) {
try {
System.loadLibrary(libName)
Log.d(TAG, " ✅ Successfully loaded: $libName")
break
} catch (e: UnsatisfiedLinkError) {
Log.w(TAG, " ❌ Failed to load $libName: ${e.message}")
}
}
// Check for CDK-FFI native libraries in APK
Log.d(TAG, "Checking APK Native Libraries:")
try {
val context = Class.forName("android.app.ActivityThread")
.getMethod("currentApplication")
.invoke(null) as android.content.Context?
if (context != null) {
val appInfo = context.applicationInfo
Log.d(TAG, " - App native library dir: ${appInfo.nativeLibraryDir}")
// Check if libcdk_ffi.so exists
val nativeLibDir = java.io.File(appInfo.nativeLibraryDir)
if (nativeLibDir.exists()) {
val files = nativeLibDir.listFiles()
Log.d(TAG, " - Native library files:")
files?.forEach { file ->
val size = if (file.isFile) " (${file.length() / 1024}KB)" else ""
Log.d(TAG, " - ${file.name}$size")
}
val cdkLib = java.io.File(nativeLibDir, "libcdk_ffi.so")
if (cdkLib.exists()) {
Log.d(TAG, " ✅ libcdk_ffi.so found: ${cdkLib.length() / 1024 / 1024}MB")
} else {
Log.w(TAG, " ❌ libcdk_ffi.so not found in native library directory")
}
} else {
Log.w(TAG, " ❌ Native library directory does not exist")
}
}
} catch (e: Exception) {
Log.w(TAG, "Could not check APK native libraries: ${e.message}")
}
// Now try to initialize CDK
Log.d(TAG, "Attempting CDK Initialization:")
try {
Log.d(TAG, " - Trying to call generateMnemonic()...")
val testMnemonic = generateMnemonic() val testMnemonic = generateMnemonic()
Log.d(TAG, " ✅ generateMnemonic() successful - mnemonic length: ${testMnemonic.split(" ").size} words")
Log.d(TAG, "✅ CDK library is fully available and functional!") Log.d(TAG, "✅ CDK library is fully available and functional!")
Log.d(TAG, "Generated test mnemonic with ${testMnemonic.split(" ").size} words")
isCdkAvailable = true isCdkAvailable = true
return true return true
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
Log.w(TAG, "UnsatisfiedLinkError during CDK initialization:") Log.w(TAG, "CDK library not available: ${e.message}")
Log.w(TAG, " Error: ${e.message}")
Log.w(TAG, " This usually means the native library (libcdk_ffi.so) is missing or incompatible")
// Try to get more details about the error
val errorMsg = e.message ?: ""
when {
errorMsg.contains("dlopen") -> Log.w(TAG, " Hint: Dynamic library loading failed")
errorMsg.contains("undefined symbol") -> Log.w(TAG, " Hint: Missing symbols in native library")
errorMsg.contains("wrong ELF class") -> Log.w(TAG, " Hint: Architecture mismatch (32-bit vs 64-bit)")
errorMsg.contains("No such file") -> Log.w(TAG, " Hint: Native library file not found")
}
} catch (e: FfiException) { } catch (e: FfiException) {
Log.w(TAG, "❌ CDK FFI Exception during initialization:") Log.w(TAG, "❌ CDK FFI error: ${e.message}")
Log.w(TAG, " Error: ${e.message}")
Log.w(TAG, " This means the library loaded but CDK functionality failed")
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
Log.w(TAG, "NoClassDefFoundError during CDK initialization:") Log.w(TAG, "CDK classes not found: ${e.message}")
Log.w(TAG, " Error: ${e.message}")
Log.w(TAG, " This usually means CDK classes are missing from classpath")
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "❌ Unexpected error during CDK initialization:") Log.w(TAG, "❌ Unexpected CDK error: ${e.message}")
Log.w(TAG, " Error type: ${e.javaClass.simpleName}")
Log.w(TAG, " Error message: ${e.message}")
Log.w(TAG, " Stack trace: ${e.stackTrace.joinToString("\n") { " $it" }}")
} }
isCdkAvailable = false isCdkAvailable = false
Log.w(TAG, "⚠️ CDK library not available - using fallback mode") Log.w(TAG, "⚠️ CDK library not available - wallet will not function properly")
Log.d(TAG, "📱 Running in demo mode with simulated operations")
Log.d(TAG, "=== END CDK LIBRARY CHECK ===") Log.d(TAG, "=== END CDK LIBRARY CHECK ===")
return false return false
} }
@@ -202,80 +76,66 @@ class CashuService {
Log.d(TAG, "Initializing wallet with mint: $mintUrl") Log.d(TAG, "Initializing wallet with mint: $mintUrl")
// Test CDK availability first // Test CDK availability first
if (initializeCdkAvailability()) { if (!initializeCdkAvailability()) {
// Real CDK implementation return@withContext Result.failure(Exception("CDK library not available"))
return@withContext initializeRealWallet(mintUrl, unit)
} else {
// Fallback implementation
return@withContext initializeFallbackWallet(mintUrl, unit)
} }
// Create database path using application context
val context = getApplicationContext()
val dbPath = "${context.filesDir}/cashu_wallet.db"
// Create local store
localStore = FfiLocalStore.newWithPath(dbPath)
Log.d(TAG, "Created CDK local store at: $dbPath")
// Generate or restore mnemonic
val mnemonic = generateMnemonic()
Log.d(TAG, "Generated wallet mnemonic")
// Convert unit string to FfiCurrencyUnit
val ffiUnit = when (unit.lowercase()) {
"sat" -> FfiCurrencyUnit.SAT
"msat" -> FfiCurrencyUnit.MSAT
"usd" -> FfiCurrencyUnit.USD
"eur" -> FfiCurrencyUnit.EUR
else -> FfiCurrencyUnit.SAT
}
// Create wallet from mnemonic
wallet = FfiWallet.fromMnemonic(
mintUrl = mintUrl,
unit = ffiUnit,
localstore = localStore!!,
mnemonicWords = mnemonic
)
Log.d(TAG, "Created CDK wallet successfully")
// Try to fetch mint info to verify connection
try {
val info = wallet?.getMintInfo()
Log.d(TAG, "Successfully connected to mint: ${info}")
} catch (e: Exception) {
Log.w(TAG, "Could not fetch mint info: ${e.message}")
// Continue anyway - wallet can work offline
}
currentMintUrl = mintUrl
currentUnit = unit
isInitialized = true
Log.d(TAG, "Real CDK wallet initialized successfully")
return@withContext Result.success(true)
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error during wallet initialization", e)
return@withContext Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to initialize wallet", e) Log.e(TAG, "Failed to initialize wallet", e)
// Fall back to demo mode return@withContext Result.failure(e)
return@withContext initializeFallbackWallet(mintUrl, unit)
} }
} }
} }
/**
* Initialize real CDK wallet
*/
private suspend fun initializeRealWallet(mintUrl: String, unit: String): Result<Boolean> {
try {
// Create local store directly using CDK-FFI
localStore = FfiLocalStore()
Log.d(TAG, "Created CDK local store")
// Generate mnemonic directly using CDK-FFI
val mnemonic = generateMnemonic()
Log.d(TAG, "Generated mnemonic for wallet")
// Convert unit string to FfiCurrencyUnit
val ffiUnit = when (unit.lowercase()) {
"sat" -> FfiCurrencyUnit.SAT
"msat" -> FfiCurrencyUnit.MSAT
"usd" -> FfiCurrencyUnit.USD
"eur" -> FfiCurrencyUnit.EUR
else -> FfiCurrencyUnit.SAT
}
// Create wallet from mnemonic directly using CDK-FFI
wallet = FfiWallet.fromMnemonic(
mintUrl = mintUrl,
unit = ffiUnit,
localstore = localStore!!,
mnemonicWords = mnemonic
)
Log.d(TAG, "Created CDK wallet successfully")
currentMintUrl = mintUrl
currentUnit = unit
isInitialized = true
Log.d(TAG, "Real CDK wallet initialized successfully")
return Result.success(true)
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error during wallet initialization", e)
return Result.failure(e)
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize real CDK wallet", e)
return Result.failure(e)
}
}
/**
* Initialize fallback wallet (demo mode)
*/
private suspend fun initializeFallbackWallet(mintUrl: String, unit: String): Result<Boolean> {
currentMintUrl = mintUrl
currentUnit = unit
isInitialized = true
mockBalance = 1000L // Demo balance
Log.d(TAG, "Demo wallet initialized with $mockBalance sats")
return Result.success(true)
}
suspend fun getMintInfo(mintUrl: String): Result<MintInfo> { suspend fun getMintInfo(mintUrl: String): Result<MintInfo> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
@@ -285,16 +145,16 @@ class CashuService {
} }
val mintInfo = if (isCdkAvailable && wallet != null) { val mintInfo = if (isCdkAvailable && wallet != null) {
// Real mint info from CDK
try { try {
val mintInfoJson = wallet!!.getMintInfo() // Get real mint info from CDK
val mintInfoData = wallet!!.getMintInfo()
Log.d(TAG, "Retrieved real mint info from CDK") Log.d(TAG, "Retrieved real mint info from CDK")
MintInfo( MintInfo(
url = mintUrl, url = mintUrl,
name = extractMintName(mintUrl), name = extractMintName(mintUrl),
description = "Real Cashu mint", description = "Cashu mint via CDK",
descriptionLong = "Real Cashu mint via CDK", descriptionLong = "Real Cashu mint connection via CDK-FFI",
contact = "", contact = "",
version = "1.0.0", version = "1.0.0",
nuts = mapOf( nuts = mapOf(
@@ -309,14 +169,10 @@ class CashuService {
) )
} catch (e: FfiException) { } catch (e: FfiException) {
Log.w(TAG, "CDK FFI error getting mint info: ${e.message}") Log.w(TAG, "CDK FFI error getting mint info: ${e.message}")
createFallbackMintInfo(mintUrl) throw e
} catch (e: Exception) {
Log.w(TAG, "Failed to get real mint info, using fallback: ${e.message}")
createFallbackMintInfo(mintUrl)
} }
} else { } else {
// Fallback mint info throw Exception("CDK not available")
createFallbackMintInfo(mintUrl)
} }
Result.success(mintInfo) Result.success(mintInfo)
@@ -327,26 +183,6 @@ class CashuService {
} }
} }
private fun createFallbackMintInfo(mintUrl: String): MintInfo {
return MintInfo(
url = mintUrl,
name = extractMintName(mintUrl) + " (Demo)",
description = "Demo Cashu mint",
descriptionLong = "Demo mode - not connected to real mint",
contact = "",
version = "0.1.0-demo",
nuts = mapOf(
"4" to "true",
"5" to "true",
"7" to "true",
"8" to "true"
),
motd = "Demo mode: Operations are simulated",
icon = null,
time = Date()
)
}
/** /**
* Get the current wallet balance * Get the current wallet balance
*/ */
@@ -357,28 +193,20 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow() initializeWallet(DEFAULT_MINT_URL).getOrThrow()
} }
val balance = if (isCdkAvailable && wallet != null) { if (!isCdkAvailable || wallet == null) {
try { return@withContext Result.failure(Exception("CDK not available"))
// Real balance from CDK
val ffiAmount = wallet!!.balance()
val balanceValue = ffiAmount.value.toLong()
Log.d(TAG, "Real balance: $balanceValue sats")
balanceValue
} catch (e: FfiException) {
Log.w(TAG, "CDK FFI error getting balance: ${e.message}")
mockBalance
} catch (e: Exception) {
Log.w(TAG, "Failed to get real balance, using demo balance: ${e.message}")
mockBalance
}
} else {
// Demo balance
Log.d(TAG, "Demo balance: $mockBalance sats")
mockBalance
} }
Result.success(balance) // Get real balance from CDK
val ffiAmount = wallet!!.balance()
val balanceValue = ffiAmount.value.toLong()
Log.d(TAG, "Real balance: $balanceValue sats")
Result.success(balanceValue)
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error getting balance: ${e.message}")
Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to get balance", e) Log.e(TAG, "Failed to get balance", e)
Result.failure(e) Result.failure(e)
@@ -396,50 +224,36 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow() initializeWallet(DEFAULT_MINT_URL).getOrThrow()
} }
val token = if (isCdkAvailable && wallet != null) { if (!isCdkAvailable || wallet == null) {
try { return@withContext Result.failure(Exception("CDK not available"))
// Real token creation with CDK
Log.d(TAG, "Creating real token for amount: $amount")
// Create send options
val sendMemo = memo?.let { FfiSendMemo(it, true) }
val sendOptions = FfiSendOptions(
memo = sendMemo,
amountSplitTarget = FfiSplitTarget.DEFAULT,
sendKind = FfiSendKind.OnlineExact,
includeFee = true,
metadata = emptyMap(),
maxProofs = null
)
// Create the token
val ffiToken = wallet!!.send(
amount = FfiAmount(amount.toULong()),
options = sendOptions,
memo = sendMemo
)
Log.d(TAG, "Created real token successfully")
ffiToken.tokenString
} catch (e: FfiException) {
Log.w(TAG, "CDK FFI error creating token: ${e.message}")
"cashuAdemo${amount}_${System.currentTimeMillis()}"
} catch (e: Exception) {
Log.w(TAG, "Failed to create real token, using demo: ${e.message}")
"cashuAdemo${amount}_${System.currentTimeMillis()}"
}
} else {
// Demo token
"cashuAdemo${amount}_${System.currentTimeMillis()}"
} }
// Simulate balance reduction for demo mode Log.d(TAG, "Creating real token for amount: $amount")
if (!isCdkAvailable) {
mockBalance = maxOf(0, mockBalance - amount)
}
Log.d(TAG, "Created token: ${token.take(20)}...") // Create send options
Result.success(token) val sendMemo = memo?.let { FfiSendMemo(it, true) }
val sendOptions = FfiSendOptions(
memo = sendMemo,
amountSplitTarget = FfiSplitTarget.DEFAULT,
sendKind = FfiSendKind.OnlineExact,
includeFee = true,
metadata = emptyMap(),
maxProofs = null
)
// Create the token
val ffiToken = wallet!!.send(
amount = FfiAmount(amount.toULong()),
options = sendOptions,
memo = sendMemo
)
Log.d(TAG, "Created real token successfully")
Result.success(ffiToken.tokenString)
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error creating token: ${e.message}")
Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to create token", e) Log.e(TAG, "Failed to create token", e)
Result.failure(e) Result.failure(e)
@@ -449,6 +263,8 @@ class CashuService {
/** /**
* Receive a Cashu token * Receive a Cashu token
* Note: CDK doesn't have explicit "receive" - tokens are auto-received when sent
* This function decodes and validates the token
*/ */
suspend fun receiveToken(token: String): Result<Long> { suspend fun receiveToken(token: String): Result<Long> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
@@ -457,24 +273,23 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow() initializeWallet(DEFAULT_MINT_URL).getOrThrow()
} }
if (!isCdkAvailable || wallet == null) {
return@withContext Result.failure(Exception("CDK not available"))
}
// Decode the token to get amount // Decode the token to get amount
val decodedToken = decodeToken(token).getOrThrow() val decodedToken = decodeToken(token).getOrThrow()
val amount = decodedToken.amount.toLong() val amount = decodedToken.amount.toLong()
if (isCdkAvailable && wallet != null) { Log.d(TAG, "Token validation successful, amount: $amount")
try {
// Real token receiving would be implemented here
Log.d(TAG, "Would receive real token, amount: $amount")
} catch (e: Exception) {
Log.w(TAG, "Failed to receive real token, using demo")
}
} else {
// Demo token receiving
mockBalance += amount
Log.d(TAG, "Demo token received, amount: $amount")
}
// In real CDK implementation, tokens are auto-received when sent
// Here we're just validating and returning the amount
Result.success(amount) Result.success(amount)
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error receiving token: ${e.message}")
Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to receive token", e) Log.e(TAG, "Failed to receive token", e)
Result.failure(e) Result.failure(e)
@@ -492,46 +307,40 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow() initializeWallet(DEFAULT_MINT_URL).getOrThrow()
} }
val mintQuote = if (isCdkAvailable && wallet != null) { if (!isCdkAvailable || wallet == null) {
try { return@withContext Result.failure(Exception("CDK not available"))
// Real mint quote creation using CDK
Log.d(TAG, "Creating real mint quote for $amount")
val ffiMintQuote = wallet!!.mintQuote(
amount = FfiAmount(amount.toULong()),
description = description
)
Log.d(TAG, "Created real mint quote: ${ffiMintQuote.id}")
// Convert FFI types to our domain types
MintQuote(
id = ffiMintQuote.id,
amount = BigDecimal(ffiMintQuote.amount.value.toLong()),
unit = ffiMintQuote.unit,
request = ffiMintQuote.request,
state = when (ffiMintQuote.state) {
FfiMintQuoteState.UNPAID -> MintQuoteState.UNPAID
FfiMintQuoteState.PAID -> MintQuoteState.PAID
FfiMintQuoteState.ISSUED -> MintQuoteState.ISSUED
},
expiry = Date(ffiMintQuote.expiry.toLong() * 1000), // Convert from seconds to milliseconds
paid = ffiMintQuote.state == FfiMintQuoteState.PAID
)
} catch (e: FfiException) {
Log.w(TAG, "CDK FFI error creating mint quote: ${e.message}")
createDemoMintQuote(amount, description)
} catch (e: Exception) {
Log.w(TAG, "Failed to create real mint quote, using demo: ${e.message}")
createDemoMintQuote(amount, description)
}
} else {
// Demo mint quote
createDemoMintQuote(amount, description)
} }
Log.d(TAG, "Creating real mint quote for $amount")
val ffiMintQuote = wallet!!.mintQuote(
amount = FfiAmount(amount.toULong()),
description = description ?: "Cashu mint"
)
Log.d(TAG, "Created real mint quote: ${ffiMintQuote.id}")
// Convert FFI types to our domain types
val mintQuote = MintQuote(
id = ffiMintQuote.id,
amount = BigDecimal(ffiMintQuote.amount.value.toLong()),
unit = ffiMintQuote.unit,
request = ffiMintQuote.request,
state = when (ffiMintQuote.state) {
FfiMintQuoteState.UNPAID -> MintQuoteState.UNPAID
FfiMintQuoteState.PAID -> MintQuoteState.PAID
FfiMintQuoteState.ISSUED -> MintQuoteState.ISSUED
},
expiry = Date(ffiMintQuote.expiry.toLong() * 1000), // Convert from seconds to milliseconds
paid = ffiMintQuote.state == FfiMintQuoteState.PAID
)
Log.d(TAG, "Created mint quote: ${mintQuote.id}") Log.d(TAG, "Created mint quote: ${mintQuote.id}")
Result.success(mintQuote) Result.success(mintQuote)
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error creating mint quote: ${e.message}")
Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to create mint quote", e) Log.e(TAG, "Failed to create mint quote", e)
Result.failure(e) Result.failure(e)
@@ -539,18 +348,6 @@ class CashuService {
} }
} }
private fun createDemoMintQuote(amount: Long, description: String?): MintQuote {
return MintQuote(
id = "demo_mint_${System.currentTimeMillis()}",
amount = BigDecimal(amount),
unit = "sat",
request = "lnbc${amount}n1demo_invoice_request",
state = MintQuoteState.UNPAID,
expiry = Date(System.currentTimeMillis() + 3600000), // 1 hour
paid = false
)
}
/** /**
* Check if a mint quote has been paid and mint ecash * Check if a mint quote has been paid and mint ecash
*/ */
@@ -561,22 +358,32 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow() initializeWallet(DEFAULT_MINT_URL).getOrThrow()
} }
val success = if (isCdkAvailable && wallet != null) { if (!isCdkAvailable || wallet == null) {
try { return@withContext Result.failure(Exception("CDK not available"))
// Real quote checking would be implemented here
Log.d(TAG, "Would check real mint quote: $quoteId")
false // Demo: not paid
} catch (e: Exception) {
Log.w(TAG, "Failed to check real quote, using demo")
false
}
} else {
// Demo: simulate not paid
false
} }
Log.d(TAG, "Mint quote $quoteId check result: $success") // Check quote state
Result.success(success) val quoteResponse = wallet!!.mintQuoteState(quoteId)
Log.d(TAG, "Checked mint quote $quoteId: ${quoteResponse.state}")
if (quoteResponse.state == FfiMintQuoteState.PAID) {
// Quote is paid, mint the tokens
try {
val mintedAmount = wallet!!.mint(quoteId, FfiSplitTarget.DEFAULT)
Log.d(TAG, "Successfully minted ${mintedAmount.value} sats for quote $quoteId")
Result.success(true)
} catch (e: FfiException) {
Log.w(TAG, "Failed to mint for paid quote: ${e.message}")
Result.success(false)
}
} else {
// Quote not paid yet
Result.success(false)
}
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error checking mint quote: ${e.message}")
Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to check mint quote", e) Log.e(TAG, "Failed to check mint quote", e)
Result.failure(e) Result.failure(e)
@@ -594,22 +401,34 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow() initializeWallet(DEFAULT_MINT_URL).getOrThrow()
} }
val meltQuote = if (isCdkAvailable && wallet != null) { if (!isCdkAvailable || wallet == null) {
try { return@withContext Result.failure(Exception("CDK not available"))
// Real melt quote creation would be implemented here
Log.d(TAG, "Would create real melt quote")
createDemoMeltQuote(invoice)
} catch (e: Exception) {
Log.w(TAG, "Failed to create real melt quote, using demo")
createDemoMeltQuote(invoice)
}
} else {
// Demo melt quote
createDemoMeltQuote(invoice)
} }
Log.d(TAG, "Creating real melt quote for invoice")
val ffiMeltQuote = wallet!!.meltQuote(invoice)
Log.d(TAG, "Created real melt quote: ${ffiMeltQuote.id}")
// Convert FFI types to our domain types
val meltQuote = MeltQuote(
id = ffiMeltQuote.id,
amount = BigDecimal(ffiMeltQuote.amount.value.toLong()),
feeReserve = BigDecimal(ffiMeltQuote.feeReserve.value.toLong()),
unit = ffiMeltQuote.unit,
request = invoice,
state = MeltQuoteState.UNPAID, // MeltQuote doesn't have state, default to UNPAID
expiry = Date(ffiMeltQuote.expiry.toLong() * 1000), // Convert from seconds to milliseconds
paid = false // Default to unpaid until melt is called
)
Log.d(TAG, "Created melt quote: ${meltQuote.id}") Log.d(TAG, "Created melt quote: ${meltQuote.id}")
Result.success(meltQuote) Result.success(meltQuote)
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error creating melt quote: ${e.message}")
Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to create melt quote", e) Log.e(TAG, "Failed to create melt quote", e)
Result.failure(e) Result.failure(e)
@@ -617,31 +436,6 @@ class CashuService {
} }
} }
private fun createDemoMeltQuote(invoice: String): MeltQuote {
// Extract amount from invoice (simplified)
val amount = if (invoice.contains("lnbc")) {
try {
// Simple parsing for demo
100L // Default amount
} catch (e: Exception) {
100L
}
} else {
100L
}
return MeltQuote(
id = "demo_melt_${System.currentTimeMillis()}",
amount = BigDecimal(amount),
feeReserve = BigDecimal(1), // 1 sat fee
unit = "sat",
request = invoice,
state = MeltQuoteState.UNPAID,
expiry = Date(System.currentTimeMillis() + 3600000), // 1 hour
paid = false
)
}
/** /**
* Pay a Lightning invoice using ecash (melt) * Pay a Lightning invoice using ecash (melt)
*/ */
@@ -652,22 +446,24 @@ class CashuService {
initializeWallet(DEFAULT_MINT_URL).getOrThrow() initializeWallet(DEFAULT_MINT_URL).getOrThrow()
} }
val success = if (isCdkAvailable && wallet != null) { if (!isCdkAvailable || wallet == null) {
try { return@withContext Result.failure(Exception("CDK not available"))
// Real payment would be implemented here
Log.d(TAG, "Would pay real invoice with quote: $quoteId")
true // Demo: simulate success
} catch (e: Exception) {
Log.w(TAG, "Failed to pay real invoice, using demo")
true
}
} else {
// Demo: simulate successful payment
true
} }
Log.d(TAG, "Invoice payment result: $success") Log.d(TAG, "Paying invoice with melt quote: $quoteId")
val meltResult = wallet!!.melt(quoteId)
Log.d(TAG, "Invoice payment result: ${meltResult.state}")
// FfiMelted.state is a String, check for success states
val success = meltResult.state.equals("paid", ignoreCase = true) ||
meltResult.state.equals("success", ignoreCase = true)
Result.success(success) Result.success(success)
} catch (e: FfiException) {
Log.e(TAG, "CDK FFI error paying invoice: ${e.message}")
Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to pay invoice", e) Log.e(TAG, "Failed to pay invoice", e)
Result.failure(e) Result.failure(e)
@@ -681,7 +477,7 @@ class CashuService {
suspend fun decodeToken(token: String): Result<CashuToken> { suspend fun decodeToken(token: String): Result<CashuToken> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
// Simple token parsing for both real and demo modes // For now, do simple parsing - CDK might have better token parsing functions
val amount = parseTokenAmount(token) val amount = parseTokenAmount(token)
val memo = parseTokenMemo(token) val memo = parseTokenMemo(token)
@@ -695,6 +491,7 @@ class CashuService {
Log.d(TAG, "Decoded token: amount=$amount") Log.d(TAG, "Decoded token: amount=$amount")
Result.success(cashuToken) Result.success(cashuToken)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to decode token", e) Log.e(TAG, "Failed to decode token", e)
Result.failure(e) Result.failure(e)
@@ -707,6 +504,8 @@ class CashuService {
*/ */
fun cleanup() { fun cleanup() {
try { try {
wallet?.destroy()
localStore?.destroy()
wallet = null wallet = null
localStore = null localStore = null
isInitialized = false isInitialized = false
@@ -726,6 +525,16 @@ class CashuService {
// Helper functions // Helper functions
private fun getApplicationContext(): Context {
return try {
val activityThread = Class.forName("android.app.ActivityThread")
val currentApplication = activityThread.getMethod("currentApplication")
currentApplication.invoke(null) as Context
} catch (e: Exception) {
throw IllegalStateException("Could not get application context", e)
}
}
private fun extractMintName(url: String): String { private fun extractMintName(url: String): String {
return try { return try {
val host = url.substringAfter("://").substringBefore("/") val host = url.substringAfter("://").substringBefore("/")
@@ -749,7 +558,8 @@ class CashuService {
} }
token.startsWith("cashuA") -> { token.startsWith("cashuA") -> {
// Real token parsing would be more complex // Real token parsing would be more complex
1000L // Default for now // For now, return default
1000L
} }
else -> 1000L else -> 1000L
} }
@@ -46,8 +46,6 @@ fun WalletOverview(
.background(Color.Black) .background(Color.Black)
.padding(16.dp) .padding(16.dp)
) { ) {
// CDK Status Banner
CdkStatusBanner(activeMint = activeMint)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -89,120 +87,6 @@ fun WalletOverview(
} }
} }
@Composable
private fun CdkStatusBanner(activeMint: String?) {
val cashuService = CashuService.getInstance()
val isCdkAvailable = cashuService.isCdkAvailable()
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = if (isCdkAvailable) Color(0xFF001a0f) else Color(0xFF1a1a00)
)
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
// CDK Logo/Icon
Card(
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(
containerColor = if (isCdkAvailable) Color(0xFF00C851) else Color(0xFFFFA500)
)
) {
Text(
text = if (isCdkAvailable) "CDK" else "DEMO",
color = Color.Black,
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp)
)
}
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = if (isCdkAvailable) "✅ Real Cashu Wallet Active" else "⚠️ Demo Mode Active",
color = if (isCdkAvailable) Color(0xFF00C851) else Color(0xFFFFA500),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
activeMint?.let { mint ->
Text(
text = if (isCdkAvailable) "Connected to: ${extractMintName(mint)}"
else "Demo wallet: ${extractMintName(mint)}",
color = Color.Gray,
fontSize = 10.sp,
fontFamily = FontFamily.Monospace
)
}
}
Spacer(modifier = Modifier.weight(1f))
// Connection status
Icon(
imageVector = if (isCdkAvailable) Icons.Filled.CheckCircle else Icons.Filled.Warning,
contentDescription = if (isCdkAvailable) "CDK Available" else "Demo Mode",
tint = if (isCdkAvailable) Color(0xFF00C851) else Color(0xFFFFA500),
modifier = Modifier.size(16.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
// Technical details
if (isCdkAvailable) {
Text(
text = "• Using Cashu Development Kit (CDK) FFI bindings",
color = Color.Gray,
fontSize = 9.sp,
fontFamily = FontFamily.Monospace
)
Text(
text = "• Real Bitcoin Lightning Network integration",
color = Color.Gray,
fontSize = 9.sp,
fontFamily = FontFamily.Monospace
)
Text(
text = "• Actual ecash minting and melting operations",
color = Color.Gray,
fontSize = 9.sp,
fontFamily = FontFamily.Monospace
)
} else {
Text(
text = "• CDK not available on this device architecture",
color = Color.Gray,
fontSize = 9.sp,
fontFamily = FontFamily.Monospace
)
Text(
text = "• Running in demonstration mode with simulated operations",
color = Color.Gray,
fontSize = 9.sp,
fontFamily = FontFamily.Monospace
)
Text(
text = "• No real Bitcoin transactions - for testing only",
color = Color.Gray,
fontSize = 9.sp,
fontFamily = FontFamily.Monospace
)
}
}
}
}
private fun extractMintName(url: String): String { private fun extractMintName(url: String): String {
return try { return try {
val host = url.substringAfter("://").substringBefore("/") val host = url.substringAfter("://").substringBefore("/")
@@ -114,7 +114,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
if (activeMintUrl.isNullOrEmpty()) { if (activeMintUrl.isNullOrEmpty()) {
Log.d(TAG, "No active mint, setting default") Log.d(TAG, "No active mint, setting default")
// Use default mint URL from CashuService // Use default mint URL from CashuService
cashuService.initializeWallet("https://mint.minibits.cash/Bitcoin").onSuccess { cashuService.initializeWallet("https://testnut.cashu.space").onSuccess {
refreshBalance() refreshBalance()
} }
} }