mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 01:45:22 +00:00
werk
This commit is contained in:
@@ -18,6 +18,9 @@
|
||||
|
||||
<!-- Haptic feedback permission -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Internet permission -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- Hardware features -->
|
||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
||||
|
||||
@@ -3,14 +3,14 @@ package com.bitchat.android.wallet.service
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import android.util.Log
|
||||
import android.content.Context
|
||||
import com.bitchat.android.wallet.data.*
|
||||
import uniffi.cdk_ffi.*
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Real Cashu service using the CDK FFI library with graceful fallback
|
||||
* when CDK is not available on the device architecture
|
||||
* Real Cashu service using the CDK FFI library
|
||||
*/
|
||||
class CashuService {
|
||||
|
||||
@@ -20,11 +20,10 @@ class CashuService {
|
||||
private var currentUnit: String = "sat"
|
||||
private var isInitialized = false
|
||||
private var isCdkAvailable = false
|
||||
private var mockBalance = 1000L // Fallback balance
|
||||
|
||||
companion object {
|
||||
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
|
||||
private var INSTANCE: CashuService? = null
|
||||
@@ -44,151 +43,26 @@ class CashuService {
|
||||
|
||||
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 {
|
||||
val jnaVersion = com.sun.jna.Native.VERSION
|
||||
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()...")
|
||||
// Test CDK availability by calling a simple function
|
||||
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, "Generated test mnemonic with ${testMnemonic.split(" ").size} words")
|
||||
isCdkAvailable = true
|
||||
return true
|
||||
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
Log.w(TAG, "❌ UnsatisfiedLinkError during CDK initialization:")
|
||||
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")
|
||||
}
|
||||
|
||||
Log.w(TAG, "❌ CDK library not available: ${e.message}")
|
||||
} catch (e: FfiException) {
|
||||
Log.w(TAG, "❌ CDK FFI Exception during initialization:")
|
||||
Log.w(TAG, " Error: ${e.message}")
|
||||
Log.w(TAG, " This means the library loaded but CDK functionality failed")
|
||||
|
||||
Log.w(TAG, "❌ CDK FFI error: ${e.message}")
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Log.w(TAG, "❌ NoClassDefFoundError during CDK initialization:")
|
||||
Log.w(TAG, " Error: ${e.message}")
|
||||
Log.w(TAG, " This usually means CDK classes are missing from classpath")
|
||||
|
||||
Log.w(TAG, "❌ CDK classes not found: ${e.message}")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "❌ Unexpected error during CDK initialization:")
|
||||
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" }}")
|
||||
Log.w(TAG, "❌ Unexpected CDK error: ${e.message}")
|
||||
}
|
||||
|
||||
isCdkAvailable = false
|
||||
Log.w(TAG, "⚠️ CDK library not available - using fallback mode")
|
||||
Log.d(TAG, "📱 Running in demo mode with simulated operations")
|
||||
Log.w(TAG, "⚠️ CDK library not available - wallet will not function properly")
|
||||
Log.d(TAG, "=== END CDK LIBRARY CHECK ===")
|
||||
return false
|
||||
}
|
||||
@@ -202,80 +76,66 @@ class CashuService {
|
||||
Log.d(TAG, "Initializing wallet with mint: $mintUrl")
|
||||
|
||||
// Test CDK availability first
|
||||
if (initializeCdkAvailability()) {
|
||||
// Real CDK implementation
|
||||
return@withContext initializeRealWallet(mintUrl, unit)
|
||||
} else {
|
||||
// Fallback implementation
|
||||
return@withContext initializeFallbackWallet(mintUrl, unit)
|
||||
if (!initializeCdkAvailability()) {
|
||||
return@withContext Result.failure(Exception("CDK library not available"))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
Log.e(TAG, "Failed to initialize wallet", e)
|
||||
// Fall back to demo mode
|
||||
return@withContext initializeFallbackWallet(mintUrl, unit)
|
||||
return@withContext Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
@@ -285,16 +145,16 @@ class CashuService {
|
||||
}
|
||||
|
||||
val mintInfo = if (isCdkAvailable && wallet != null) {
|
||||
// Real mint info from CDK
|
||||
try {
|
||||
val mintInfoJson = wallet!!.getMintInfo()
|
||||
// Get real mint info from CDK
|
||||
val mintInfoData = wallet!!.getMintInfo()
|
||||
Log.d(TAG, "Retrieved real mint info from CDK")
|
||||
|
||||
MintInfo(
|
||||
url = mintUrl,
|
||||
name = extractMintName(mintUrl),
|
||||
description = "Real Cashu mint",
|
||||
descriptionLong = "Real Cashu mint via CDK",
|
||||
description = "Cashu mint via CDK",
|
||||
descriptionLong = "Real Cashu mint connection via CDK-FFI",
|
||||
contact = "",
|
||||
version = "1.0.0",
|
||||
nuts = mapOf(
|
||||
@@ -309,14 +169,10 @@ class CashuService {
|
||||
)
|
||||
} catch (e: FfiException) {
|
||||
Log.w(TAG, "CDK FFI error getting mint info: ${e.message}")
|
||||
createFallbackMintInfo(mintUrl)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to get real mint info, using fallback: ${e.message}")
|
||||
createFallbackMintInfo(mintUrl)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
// Fallback mint info
|
||||
createFallbackMintInfo(mintUrl)
|
||||
throw Exception("CDK not available")
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -357,28 +193,20 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
val balance = if (isCdkAvailable && wallet != null) {
|
||||
try {
|
||||
// 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
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
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) {
|
||||
Log.e(TAG, "Failed to get balance", e)
|
||||
Result.failure(e)
|
||||
@@ -396,50 +224,36 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
val token = if (isCdkAvailable && wallet != null) {
|
||||
try {
|
||||
// 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()}"
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
// Simulate balance reduction for demo mode
|
||||
if (!isCdkAvailable) {
|
||||
mockBalance = maxOf(0, mockBalance - amount)
|
||||
}
|
||||
Log.d(TAG, "Creating real token for amount: $amount")
|
||||
|
||||
Log.d(TAG, "Created token: ${token.take(20)}...")
|
||||
Result.success(token)
|
||||
// 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")
|
||||
Result.success(ffiToken.tokenString)
|
||||
|
||||
} catch (e: FfiException) {
|
||||
Log.e(TAG, "CDK FFI error creating token: ${e.message}")
|
||||
Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to create token", e)
|
||||
Result.failure(e)
|
||||
@@ -449,6 +263,8 @@ class CashuService {
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
@@ -457,24 +273,23 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
// Decode the token to get amount
|
||||
val decodedToken = decodeToken(token).getOrThrow()
|
||||
val amount = decodedToken.amount.toLong()
|
||||
|
||||
if (isCdkAvailable && wallet != null) {
|
||||
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")
|
||||
}
|
||||
Log.d(TAG, "Token validation successful, amount: $amount")
|
||||
|
||||
// In real CDK implementation, tokens are auto-received when sent
|
||||
// Here we're just validating and returning the amount
|
||||
Result.success(amount)
|
||||
|
||||
} catch (e: FfiException) {
|
||||
Log.e(TAG, "CDK FFI error receiving token: ${e.message}")
|
||||
Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to receive token", e)
|
||||
Result.failure(e)
|
||||
@@ -492,46 +307,40 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
val mintQuote = if (isCdkAvailable && wallet != null) {
|
||||
try {
|
||||
// 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)
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
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}")
|
||||
Result.success(mintQuote)
|
||||
|
||||
} catch (e: FfiException) {
|
||||
Log.e(TAG, "CDK FFI error creating mint quote: ${e.message}")
|
||||
Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to create mint quote", 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
|
||||
*/
|
||||
@@ -561,22 +358,32 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
val success = if (isCdkAvailable && wallet != null) {
|
||||
try {
|
||||
// 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
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
Log.d(TAG, "Mint quote $quoteId check result: $success")
|
||||
Result.success(success)
|
||||
// Check quote state
|
||||
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) {
|
||||
Log.e(TAG, "Failed to check mint quote", e)
|
||||
Result.failure(e)
|
||||
@@ -594,22 +401,34 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
val meltQuote = if (isCdkAvailable && wallet != null) {
|
||||
try {
|
||||
// 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)
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
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}")
|
||||
Result.success(meltQuote)
|
||||
|
||||
} catch (e: FfiException) {
|
||||
Log.e(TAG, "CDK FFI error creating melt quote: ${e.message}")
|
||||
Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to create melt quote", 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)
|
||||
*/
|
||||
@@ -652,22 +446,24 @@ class CashuService {
|
||||
initializeWallet(DEFAULT_MINT_URL).getOrThrow()
|
||||
}
|
||||
|
||||
val success = if (isCdkAvailable && wallet != null) {
|
||||
try {
|
||||
// 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
|
||||
if (!isCdkAvailable || wallet == null) {
|
||||
return@withContext Result.failure(Exception("CDK not available"))
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
} catch (e: FfiException) {
|
||||
Log.e(TAG, "CDK FFI error paying invoice: ${e.message}")
|
||||
Result.failure(e)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to pay invoice", e)
|
||||
Result.failure(e)
|
||||
@@ -681,7 +477,7 @@ class CashuService {
|
||||
suspend fun decodeToken(token: String): Result<CashuToken> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
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 memo = parseTokenMemo(token)
|
||||
|
||||
@@ -695,6 +491,7 @@ class CashuService {
|
||||
|
||||
Log.d(TAG, "Decoded token: amount=$amount")
|
||||
Result.success(cashuToken)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decode token", e)
|
||||
Result.failure(e)
|
||||
@@ -707,6 +504,8 @@ class CashuService {
|
||||
*/
|
||||
fun cleanup() {
|
||||
try {
|
||||
wallet?.destroy()
|
||||
localStore?.destroy()
|
||||
wallet = null
|
||||
localStore = null
|
||||
isInitialized = false
|
||||
@@ -726,6 +525,16 @@ class CashuService {
|
||||
|
||||
// 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 {
|
||||
return try {
|
||||
val host = url.substringAfter("://").substringBefore("/")
|
||||
@@ -749,7 +558,8 @@ class CashuService {
|
||||
}
|
||||
token.startsWith("cashuA") -> {
|
||||
// Real token parsing would be more complex
|
||||
1000L // Default for now
|
||||
// For now, return default
|
||||
1000L
|
||||
}
|
||||
else -> 1000L
|
||||
}
|
||||
|
||||
@@ -46,8 +46,6 @@ fun WalletOverview(
|
||||
.background(Color.Black)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// CDK Status Banner
|
||||
CdkStatusBanner(activeMint = activeMint)
|
||||
|
||||
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 {
|
||||
return try {
|
||||
val host = url.substringAfter("://").substringBefore("/")
|
||||
|
||||
@@ -114,7 +114,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||
if (activeMintUrl.isNullOrEmpty()) {
|
||||
Log.d(TAG, "No active mint, setting default")
|
||||
// Use default mint URL from CashuService
|
||||
cashuService.initializeWallet("https://mint.minibits.cash/Bitcoin").onSuccess {
|
||||
cashuService.initializeWallet("https://testnut.cashu.space").onSuccess {
|
||||
refreshBalance()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user