feat: parse bolt11 locally with network fallback

This commit is contained in:
2026-06-04 17:15:34 +02:00
parent 4dadc75aca
commit 8e53a927a0
8 changed files with 627 additions and 26 deletions
@@ -2,6 +2,7 @@ package com.bitcointxoko.gudariwallet.data
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.api.RouteHintHop
import java.time.Instant
/**
* Domain model classes for the wallet layer.
@@ -24,7 +25,11 @@ data class DecodedBolt11(
val amountSats : Long,
val memo : String,
val payee : String?,
val routeHints : List<RouteHintHop> = emptyList()
val routeHints : List<RouteHintHop> = emptyList(),
val paymentHash : String, // hex; unique receipt ID (required field)
val createdAt : Instant, // invoice creation timestamp
val expiresAt : Instant, // createdAt + expiry (default +1h); show countdown
val fallbackAddress : String? = null // on-chain fallback address, if present
)
data class ScannedLnurl(
@@ -4,9 +4,11 @@ import android.net.Uri
import android.util.Log
import com.bitcointxoko.gudariwallet.api.*
import com.bitcointxoko.gudariwallet.security.SecretStore
import com.bitcointxoko.gudariwallet.util.Bolt11Decoder
import com.bitcointxoko.gudariwallet.util.LnurlBech32
import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser
import com.bitcointxoko.gudariwallet.util.WalletConstants
import java.time.Instant
private const val TAG = "LNbitsWalletRepo"
@@ -47,22 +49,51 @@ class LNbitsWalletRepository(
// ── Send ──────────────────────────────────────────────────────────────────
override suspend fun decodeBolt11(bolt11: String): DecodedBolt11 {
// ── 1. Try local decode first (no network, no latency) ───────────────
val local = runCatching { Bolt11Decoder.decode(bolt11) }.getOrNull()
if (local != null) {
Log.i("WalletRepository", "decodeBolt11: decoded locally ✓")
return local
}
// ── 2. Fall back to network API ───────────────────────────────────────
Log.w("WalletRepository", "decodeBolt11: local decode failed, falling back to network")
val response = api.decodeInvoice(secrets.invoiceKey(), DecodeInvoiceRequest(data = bolt11))
Log.i("WalletRepository", "decodeBolt11: decoded via network ✓")
val amountSats = response.amountMsat
?.takeIf { it > 0L }
?.div(WalletConstants.MSAT_PER_SAT)
?: throw UnsupportedOperationException(
"Zero-amount invoices are not supported. Please ask the sender to specify an amount."
)
val paymentHash = response.paymentHash
?: throw UnsupportedOperationException(
"Invoice is missing a payment hash and cannot be paid."
)
// createdAt / expiresAt / fallbackAddress are not returned by the API —
// use safe defaults and warn so we know if this path is hit in practice.
Log.w("WalletRepository", "decodeBolt11: network response has no timestamp/expiry — " +
"using Instant.now() + 1h as fallback")
val createdAt = Instant.now()
val expiresAt = createdAt.plusSeconds(3600L)
return DecodedBolt11(
amountSats = amountSats,
memo = response.description ?: "",
payee = response.payee?.takeIf { it.isNotBlank() },
routeHints = response.routeHints?.flatten() ?: emptyList()
amountSats = amountSats,
memo = response.description ?: "",
payee = response.payee?.takeIf { it.isNotBlank() },
routeHints = response.routeHints?.flatten() ?: emptyList(),
paymentHash = paymentHash,
createdAt = createdAt,
expiresAt = expiresAt,
fallbackAddress = null
)
}
override suspend fun scanLnurl(code: String): ScannedLnurl {
val r = api.lnurlScan(secrets.invoiceKey(), code)
return ScannedLnurl(