Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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 @@ -134,7 +134,7 @@ dependencies {
implementation("com.google.zxing:core:3.5.3")

// CDK Kotlin bindings
implementation("org.cashudevkit:cdk-kotlin:0.15.1")
implementation("org.cashudevkit:cdk-kotlin:0.15.2-rc.0")

// ML Kit Barcode Scanning
implementation("com.google.mlkit:barcode-scanning:17.3.0")
Expand Down
226 changes: 177 additions & 49 deletions app/src/main/java/com/electricdreams/numo/PaymentRequestActivity.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import org.cashudevkit.WalletDatabaseImpl
import org.cashudevkit.NoPointer
import org.cashudevkit.WalletSqliteDatabase
import org.cashudevkit.WalletRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.cashudevkit.generateMnemonic

/**
Expand All @@ -42,6 +45,9 @@ object CashuWalletManager : MintManager.MintChangeListener {
@Volatile
private var wallet: WalletRepository? = null

private val _isWalletReady = MutableStateFlow(false)
val isWalletReady: StateFlow<Boolean> = _isWalletReady.asStateFlow()

/** Initialize from ModernPOSActivity. Safe to call multiple times. */
fun init(context: Context) {
if (this::appContext.isInitialized) return
Expand Down Expand Up @@ -159,6 +165,7 @@ object CashuWalletManager : MintManager.MintChangeListener {

database = db
wallet = newWallet
_isWalletReady.value = true

Log.d(TAG, "Wallet restore complete. Restored ${mints.size} mints.")
return balanceChanges
Expand Down Expand Up @@ -437,10 +444,12 @@ object CashuWalletManager : MintManager.MintChangeListener {

database = db
wallet = newWallet
_isWalletReady.value = true

Log.d(TAG, "Initialized WalletRepository with ${'$'}{mints.size} mints; DB=${'$'}{dbFile.absolutePath}")
} catch (t: Throwable) {
Log.e(TAG, "Failed to initialize WalletRepository", t)
_isWalletReady.value = false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ object CashuPaymentHelper {
* Structured result of validating a Cashu token against an expected
* amount and the merchant's allowed mint list.
*/
data class GeneratedPaymentRequest(
val original: String,
val bech32: String
)

sealed class TokenValidationResult {
/** The provided string was not a valid Cashu token. */
object InvalidFormat : TokenValidationResult()
Expand Down Expand Up @@ -68,7 +73,7 @@ object CashuPaymentHelper {
amount: Long,
description: String?,
allowedMints: List<String>?,
): String? {
): GeneratedPaymentRequest? {
return try {
val paymentRequest = PaymentRequest().apply {
this.amount = Optional.of(amount)
Expand All @@ -91,15 +96,23 @@ object CashuPaymentHelper {

val encoded = paymentRequest.encode()
Log.d(TAG, "Created payment request: $encoded")
encoded
try {
val cdkRequest = org.cashudevkit.PaymentRequest.fromString(encoded)
val bech32 = cdkRequest.toBech32String()
Log.d(TAG, "Converted to bech32: $bech32")
GeneratedPaymentRequest(encoded, bech32)
} catch (e: Throwable) {
Log.e(TAG, "Error converting to bech32, returning CREQA format: ${e.message}", e)
GeneratedPaymentRequest(encoded, encoded)
}
} catch (e: Exception) {
Log.e(TAG, "Error creating payment request: ${e.message}", e)
null
}
}

@JvmStatic
fun createPaymentRequest(amount: Long, description: String?): String? =
fun createPaymentRequest(amount: Long, description: String?): GeneratedPaymentRequest? =
createPaymentRequest(amount, description, null)

@JvmStatic
Expand All @@ -108,7 +121,7 @@ object CashuPaymentHelper {
description: String?,
allowedMints: List<String>?,
nprofile: String,
): String? {
): GeneratedPaymentRequest? {
return try {
val paymentRequest = PaymentRequest().apply {
this.amount = Optional.of(amount)
Expand Down Expand Up @@ -144,7 +157,15 @@ object CashuPaymentHelper {

val encoded = paymentRequest.encode()
Log.d(TAG, "Created Nostr payment request: $encoded")
encoded
try {
val cdkRequest = org.cashudevkit.PaymentRequest.fromString(encoded)
val bech32 = cdkRequest.toBech32String()
Log.d(TAG, "Converted Nostr to bech32: $bech32")
GeneratedPaymentRequest(encoded, bech32)
} catch (e: Throwable) {
Log.e(TAG, "Error converting to bech32, returning CREQA format: ${e.message}", e)
GeneratedPaymentRequest(encoded, encoded)
}
} catch (e: Exception) {
Log.e(TAG, "Error creating Nostr payment request: ${e.message}", e)
null
Expand Down Expand Up @@ -233,7 +254,7 @@ object CashuPaymentHelper {

@JvmStatic
fun isCashuPaymentRequest(text: String?): Boolean =
text != null && text.startsWith("creqA")
text != null && (text.startsWith("creqA") || text.lowercase().startsWith("creqb"))

// === Validation using CDK Token ========================================

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,13 @@ public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
Log.i(TAG, "Command: " + description);
}

// Start/reset NFC reading indicator when we receive APDU commands
// Only track reading if we have a payment callback set (meaning we're expecting a payment)
if (paymentCallback != null) {
startOrResetNfcReading();
// Check if this is an UPDATE BINARY command (writing to NDEF)
// If so, and we have a callback, trigger the NFC reading UI
if (paymentCallback != null && commandApdu != null && commandApdu.length >= 2) {
// UPDATE BINARY header is 00 D6
if (commandApdu[0] == (byte)0x00 && commandApdu[1] == (byte)0xD6) {
startOrResetNfcReading();
}
}

// Try to process with the NDEF processor
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.electricdreams.numo.payment

import android.content.Context
import android.util.Log
import com.electricdreams.numo.R
import com.electricdreams.numo.core.cashu.CashuWalletManager
import com.google.gson.Gson
import com.google.gson.JsonObject
Expand Down Expand Up @@ -43,6 +45,7 @@ import kotlin.coroutines.resumeWithException
* @param uiScope Coroutine scope for UI callbacks
*/
class LightningMintHandler(
private val context: Context,
private val preferredMint: String?,
private val allowedMints: List<String>,
private val uiScope: CoroutineScope,
Expand All @@ -51,10 +54,11 @@ class LightningMintHandler(
) {
// Secondary constructor to maintain compatibility
constructor(
context: Context,
preferredMint: String?,
allowedMints: List<String>,
uiScope: CoroutineScope
) : this(preferredMint, allowedMints, uiScope, Dispatchers.IO)
) : this(context, preferredMint, allowedMints, uiScope, Dispatchers.IO)
/**
* Callback interface for Lightning mint events.
*/
Expand Down Expand Up @@ -149,7 +153,7 @@ class LightningMintHandler(

Log.d(TAG, "Requesting Lightning mint quote from ${mintUrl.url} for $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)
val quote = mintWallet?.mintQuote(org.cashudevkit.PaymentMethod.Bolt11, quoteAmount, context.getString(R.string.payment_request_lightning_description, paymentAmount), null)
?: throw Exception("Failed to get wallet for mint: ${mintUrl.url}")
mintQuote = quote

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.electricdreams.numo.payment

import android.content.Context
import com.electricdreams.numo.R
import android.util.Log
import com.electricdreams.numo.feature.history.PaymentsHistoryActivity
import com.electricdreams.numo.core.util.MintManager
Expand Down Expand Up @@ -51,6 +52,10 @@ class NostrPaymentHandler(
var paymentRequest: String? = null
private set

/** The generated payment request string (bech32) */
var paymentRequestBech32: String? = null
private set

/**
* Start a new Nostr payment flow with fresh keys.
*
Expand Down Expand Up @@ -136,7 +141,7 @@ class NostrPaymentHandler(
// Create payment request with Nostr transport
val request = CashuPaymentHelper.createPaymentRequestWithNostr(
paymentAmount,
"Payment of $paymentAmount sats",
context.getString(R.string.payment_request_default_description, paymentAmount),
mintsForPaymentRequest,
profile
)
Expand All @@ -147,9 +152,10 @@ class NostrPaymentHandler(
return
}

paymentRequest = request
Log.d(TAG, "Created payment request with Nostr: $request")
callback.onPaymentRequestReady(request)
paymentRequest = request.original
paymentRequestBech32 = request.bech32
Log.d(TAG, "Created payment request with Nostr: ${request.original}")
callback.onPaymentRequestReady(request.original)

// Stop any existing listener
listener?.stop()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class PaymentMethodHandler(
val mintsForPaymentRequest =
if (mintManager.isSwapFromUnknownMintsEnabled()) null else allowedMints

val paymentRequest = CashuPaymentHelper.createPaymentRequest(amount, "Payment of $amount sats", mintsForPaymentRequest)
val paymentRequest = CashuPaymentHelper.createPaymentRequest(amount, "Payment of $amount sats", mintsForPaymentRequest)?.original
?: run {
Toast.makeText(activity, "Failed to create payment request", Toast.LENGTH_SHORT).show()
return
Expand Down
Loading