refactor: move logs to timber

This commit is contained in:
2026-06-08 19:07:09 +02:00
parent ceed94b253
commit f4b74ee43d
37 changed files with 267 additions and 258 deletions
+5 -2
View File
@@ -27,7 +27,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "0.1.0"
// testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" // testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner" testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
@@ -160,11 +160,14 @@ dependencies {
// secp256k1 // secp256k1
implementation(libs.secp256k1.kmp.android) implementation(libs.secp256k1.kmp.android)
// lyricist
implementation(libs.lyricist) implementation(libs.lyricist)
// Required for @LyricistStrings code generation // Required for @LyricistStrings code generation
ksp(libs.lyricist.processor) ksp(libs.lyricist.processor)
// timber
implementation(libs.timber)
} }
protobuf { protobuf {
@@ -9,6 +9,7 @@ import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
import com.bitcointxoko.gudariwallet.sync.HistoricalSyncWorkerFactory import com.bitcointxoko.gudariwallet.sync.HistoricalSyncWorkerFactory
import com.bitcointxoko.gudariwallet.util.NotificationHelper import com.bitcointxoko.gudariwallet.util.NotificationHelper
import timber.log.Timber
class GudariWalletApplication : Application(), Configuration.Provider { class GudariWalletApplication : Application(), Configuration.Provider {
@@ -29,5 +30,8 @@ class GudariWalletApplication : Application(), Configuration.Provider {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
NotificationHelper.createChannels(this) // moved from MainActivity NotificationHelper.createChannels(this) // moved from MainActivity
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
} }
} }
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
@@ -47,7 +47,7 @@ class BalancePrefsStore(context: Context) {
val prefs = dataStore.data val prefs = dataStore.data
.catch { e -> .catch { e ->
if (e is IOException) { if (e is IOException) {
Log.w(TAG, "BALANCE [SATS ] read error — ${e.message}") Timber.w("BALANCE [SATS ] read error — ${e.message}")
emit(emptyPreferences()) emit(emptyPreferences())
} else throw e } else throw e
} }
@@ -62,7 +62,7 @@ class BalancePrefsStore(context: Context) {
val prefs = dataStore.data val prefs = dataStore.data
.catch { e -> .catch { e ->
if (e is IOException) { if (e is IOException) {
Log.w(TAG, "BALANCE [SAVED_AT] read error — ${e.message}") Timber.w("BALANCE [SAVED_AT] read error — ${e.message}")
emit(emptyPreferences()) emit(emptyPreferences())
} else throw e } else throw e
} }
@@ -80,9 +80,9 @@ class BalancePrefsStore(context: Context) {
prefs[BALANCE_SAVED_AT] = System.currentTimeMillis() prefs[BALANCE_SAVED_AT] = System.currentTimeMillis()
} }
}.onFailure { e -> }.onFailure { e ->
Log.w(TAG, "BALANCE [SATS ] persist failed — ${e.message}") Timber.w("BALANCE [SATS ] persist failed — ${e.message}")
} }
Log.d(TAG, "BALANCE [SATS ] $sats sats — persisted to DataStore") Timber.d("BALANCE [SATS ] $sats sats — persisted to DataStore")
} }
// ── Balance visibility ──────────────────────────────────────────────────── // ── Balance visibility ────────────────────────────────────────────────────
@@ -94,7 +94,7 @@ class BalancePrefsStore(context: Context) {
val isBalanceHiddenFlow: Flow<Boolean> = dataStore.data val isBalanceHiddenFlow: Flow<Boolean> = dataStore.data
.catch { e -> .catch { e ->
if (e is IOException) { if (e is IOException) {
Log.w(TAG, "BALANCE [HIDDEN ] read error — ${e.message}") Timber.w("BALANCE [HIDDEN ] read error — ${e.message}")
emit(emptyPreferences()) emit(emptyPreferences())
} else throw e } else throw e
} }
@@ -104,7 +104,7 @@ class BalancePrefsStore(context: Context) {
runCatching { runCatching {
dataStore.edit { prefs -> prefs[BALANCE_HIDDEN] = hidden } dataStore.edit { prefs -> prefs[BALANCE_HIDDEN] = hidden }
}.onFailure { e -> }.onFailure { e ->
Log.w(TAG, "BALANCE [HIDDEN ] persist failed — ${e.message}") Timber.w("BALANCE [HIDDEN ] persist failed — ${e.message}")
} }
} }
@@ -114,7 +114,7 @@ class BalancePrefsStore(context: Context) {
val prefs = dataStore.data val prefs = dataStore.data
.catch { e -> .catch { e ->
if (e is IOException) { if (e is IOException) {
Log.w(TAG, "BALANCE [CURRENCY] read error — ${e.message}") Timber.w("BALANCE [CURRENCY] read error — ${e.message}")
emit(emptyPreferences()) emit(emptyPreferences())
} else throw e } else throw e
} }
@@ -129,7 +129,7 @@ class BalancePrefsStore(context: Context) {
else prefs.remove(FIAT_CURRENCY) else prefs.remove(FIAT_CURRENCY)
} }
}.onFailure { e -> }.onFailure { e ->
Log.w(TAG, "BALANCE [CURRENCY] persist failed — ${e.message}") Timber.w("BALANCE [CURRENCY] persist failed — ${e.message}")
} }
} }
} }
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
@@ -45,14 +45,14 @@ class FiatDataStore(context: Context) {
val prefs = dataStore.data val prefs = dataStore.data
.catch { e -> .catch { e ->
if (e is IOException) { if (e is IOException) {
Log.w(TAG, "FIAT [RATE STALE ] read error — ${e.message}") Timber.w("FIAT [RATE STALE ] read error — ${e.message}")
emit(emptyPreferences()) emit(emptyPreferences())
} else throw e } else throw e
} }
.first() .first()
val value = prefs[rateKey(currency)]?.toDouble() val value = prefs[rateKey(currency)]?.toDouble()
if (value != null && value > 0.0) { if (value != null && value > 0.0) {
Log.d(TAG, "FIAT [RATE STALE ] $currency — using persisted rate $value") Timber.d("FIAT [RATE STALE ] $currency — using persisted rate $value")
} }
return value?.takeIf { it > 0.0 } return value?.takeIf { it > 0.0 }
} }
@@ -68,9 +68,9 @@ class FiatDataStore(context: Context) {
prefs[rateFetchedAtKey(currency)] = System.currentTimeMillis() prefs[rateFetchedAtKey(currency)] = System.currentTimeMillis()
} }
}.onFailure { e -> }.onFailure { e ->
Log.w(TAG, "FIAT [RATE FRESH ] persist failed for $currency${e.message}") Timber.w("FIAT [RATE FRESH ] persist failed for $currency${e.message}")
} }
Log.d(TAG, "FIAT [RATE FRESH ] $currency = $rate — persisted to DataStore") Timber.d("FIAT [RATE FRESH ] $currency = $rate — persisted to DataStore")
} }
// ── Currency list ───────────────────────────────────────────────────────── // ── Currency list ─────────────────────────────────────────────────────────
@@ -82,7 +82,7 @@ class FiatDataStore(context: Context) {
val prefs = dataStore.data val prefs = dataStore.data
.catch { e -> .catch { e ->
if (e is IOException) { if (e is IOException) {
Log.w(TAG, "FIAT [CURRENCIES] read error — ${e.message}") Timber.w("FIAT [CURRENCIES] read error — ${e.message}")
emit(emptyPreferences()) emit(emptyPreferences())
} else throw e } else throw e
} }
@@ -91,10 +91,10 @@ class FiatDataStore(context: Context) {
return runCatching { return runCatching {
val type = object : TypeToken<List<String>>() {}.type val type = object : TypeToken<List<String>>() {}.type
val list: List<String> = gson.fromJson(json, type) val list: List<String> = gson.fromJson(json, type)
Log.d(TAG, "FIAT [CURRENCIES] DataStore hit (${list.size} currencies)") Timber.d("FIAT [CURRENCIES] DataStore hit (${list.size} currencies)")
list list
}.getOrElse { e -> }.getOrElse { e ->
Log.w(TAG, "FIAT [CURRENCIES] parse failed — ${e.message}") Timber.w("FIAT [CURRENCIES] parse failed — ${e.message}")
null null
} }
} }
@@ -109,8 +109,8 @@ class FiatDataStore(context: Context) {
prefs[currenciesKey] = gson.toJson(currencies) prefs[currenciesKey] = gson.toJson(currencies)
} }
}.onFailure { e -> }.onFailure { e ->
Log.w(TAG, "FIAT [CURRENCIES] persist failed — ${e.message}") Timber.w("FIAT [CURRENCIES] persist failed — ${e.message}")
} }
Log.d(TAG, "FIAT [CURRENCIES] persisted ${currencies.size} currencies to DataStore") Timber.d("FIAT [CURRENCIES] persisted ${currencies.size} currencies to DataStore")
} }
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import android.util.Log import timber.log.Timber
private const val TAG = "FiatRateCache" private const val TAG = "FiatRateCache"
@@ -29,14 +29,14 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
val age = now - rateLastFetchedAt val age = now - rateLastFetchedAt
if (cachedRate != null && cachedRateCurrency == currency && age < ttlMs) { if (cachedRate != null && cachedRateCurrency == currency && age < ttlMs) {
Log.d(TAG, "FIAT [RATE CACHE ] $currency — in-memory hit (age ${age / 1000}s)") Timber.d("FIAT [RATE CACHE ] $currency — in-memory hit (age ${age / 1000}s)")
return cachedRate return cachedRate
} }
// Cold-start fallback: last persisted rate from DataStore // Cold-start fallback: last persisted rate from DataStore
val persisted = fiatDataStore.getPersistedRate(currency) val persisted = fiatDataStore.getPersistedRate(currency)
if (persisted != null) { if (persisted != null) {
Log.d(TAG, "FIAT [RATE STALE ] $currency — using persisted rate $persisted") Timber.d("FIAT [RATE STALE ] $currency — using persisted rate $persisted")
return persisted return persisted
} }
@@ -52,7 +52,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
rateLastFetchedAt = now rateLastFetchedAt = now
cachedRateCurrency = currency cachedRateCurrency = currency
fiatDataStore.persistRate(currency, rate) fiatDataStore.persistRate(currency, rate)
Log.d(TAG, "FIAT [RATE FRESH ] 1 $currency = $rate sats — cached + persisted") Timber.d("FIAT [RATE FRESH ] 1 $currency = $rate sats — cached + persisted")
} }
/** Clears the in-memory rate (e.g. when the user switches currency). */ /** Clears the in-memory rate (e.g. when the user switches currency). */
@@ -72,7 +72,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
*/ */
suspend fun getCurrencies(): List<String>? { suspend fun getCurrencies(): List<String>? {
cachedCurrencies?.let { cachedCurrencies?.let {
Log.d(TAG, "FIAT [CURRENCIES] in-memory hit (${it.size} currencies)") Timber.d("FIAT [CURRENCIES] in-memory hit (${it.size} currencies)")
return it return it
} }
@@ -88,7 +88,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
if (currencies.isEmpty()) return if (currencies.isEmpty()) return
cachedCurrencies = currencies cachedCurrencies = currencies
fiatDataStore.persistCurrencies(currencies) fiatDataStore.persistCurrencies(currencies)
Log.d(TAG, "FIAT [CURRENCIES] cached ${currencies.size} currencies") Timber.d("FIAT [CURRENCIES] cached ${currencies.size} currencies")
} }
val currentRate: Double? val currentRate: Double?
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import android.net.Uri import android.net.Uri
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.* import com.bitcointxoko.gudariwallet.api.*
import com.bitcointxoko.gudariwallet.security.SecretStore import com.bitcointxoko.gudariwallet.security.SecretStore
import com.bitcointxoko.gudariwallet.util.Bolt11Decoder import com.bitcointxoko.gudariwallet.util.Bolt11Decoder
@@ -52,14 +52,14 @@ class LNbitsWalletRepository(
// ── 1. Try local decode first (no network, no latency) ─────────────── // ── 1. Try local decode first (no network, no latency) ───────────────
val local = runCatching { Bolt11Decoder.decode(bolt11) }.getOrNull() val local = runCatching { Bolt11Decoder.decode(bolt11) }.getOrNull()
if (local != null) { if (local != null) {
Log.i("WalletRepository", "decodeBolt11: decoded locally ✓") Timber.i("decodeBolt11: decoded locally ✓")
return local return local
} }
// ── 2. Fall back to network API ─────────────────────────────────────── // ── 2. Fall back to network API ───────────────────────────────────────
Log.w("WalletRepository", "decodeBolt11: local decode failed, falling back to network") Timber.w("decodeBolt11: local decode failed, falling back to network")
val response = api.decodeInvoice(secrets.invoiceKey(), DecodeInvoiceRequest(data = bolt11)) val response = api.decodeInvoice(secrets.invoiceKey(), DecodeInvoiceRequest(data = bolt11))
Log.i("WalletRepository", "decodeBolt11: decoded via network ✓") Timber.i("decodeBolt11: decoded via network ✓")
val amountSats = response.amountMsat val amountSats = response.amountMsat
?.takeIf { it > 0L } ?.takeIf { it > 0L }
@@ -75,7 +75,7 @@ class LNbitsWalletRepository(
// createdAt / expiresAt / fallbackAddress are not returned by the API — // createdAt / expiresAt / fallbackAddress are not returned by the API —
// use safe defaults and warn so we know if this path is hit in practice. // 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 — " + Timber.w("decodeBolt11: network response has no timestamp/expiry — " +
"using Instant.now() + 1h as fallback") "using Instant.now() + 1h as fallback")
val createdAt = Instant.now() val createdAt = Instant.now()
val expiresAt = createdAt.plusSeconds(3600L) val expiresAt = createdAt.plusSeconds(3600L)
@@ -134,7 +134,7 @@ class LNbitsWalletRepository(
} }
} }
Log.d(TAG, "LNURL [DIRECT SCAN] $code$httpsUrl") Timber.d("LNURL [DIRECT SCAN] $code$httpsUrl")
val r = api.fetchLnurlMetadata(httpsUrl) val r = api.fetchLnurlMetadata(httpsUrl)
return ScannedLnurl( return ScannedLnurl(
@@ -160,7 +160,7 @@ class LNbitsWalletRepository(
comment : String? comment : String?
): PaymentConfirmation { ): PaymentConfirmation {
val bolt11 = fetchLnurlCallbackInvoice(rawRes, amountMsat, comment) val bolt11 = fetchLnurlCallbackInvoice(rawRes, amountMsat, comment)
Log.d(TAG, "LNURL [DIRECT PAY ] got invoice, paying bolt11") Timber.d("LNURL [DIRECT PAY ] got invoice, paying bolt11")
return payBolt11(bolt11) return payBolt11(bolt11)
} }
@@ -186,7 +186,7 @@ class LNbitsWalletRepository(
append(Uri.encode(comment)) append(Uri.encode(comment))
} }
} }
Log.d(TAG, "LNURL [FETCH INVOICE] callback=$callbackUrl") Timber.d("LNURL [FETCH INVOICE] callback=$callbackUrl")
val callbackResponse = api.fetchLnurlCallback(callbackUrl) val callbackResponse = api.fetchLnurlCallback(callbackUrl)
if (callbackResponse.status == "ERROR") { if (callbackResponse.status == "ERROR") {
throw RuntimeException( throw RuntimeException(
@@ -246,7 +246,7 @@ class LNbitsWalletRepository(
append("&pr=") append("&pr=")
append(Uri.encode(bolt11)) append(Uri.encode(bolt11))
} }
Log.d(TAG, "LNURL-WITHDRAW [CALLBACK] $url") Timber.d("LNURL-WITHDRAW [CALLBACK] $url")
return api.withdrawCallback(url) return api.withdrawCallback(url)
} }
@@ -265,12 +265,12 @@ class LNbitsWalletRepository(
// ── Fiat ────────────────────────────────────────────────────────────────── // ── Fiat ──────────────────────────────────────────────────────────────────
override suspend fun getFiatRate(currency: String): Double { override suspend fun getFiatRate(currency: String): Double {
Log.d(TAG, "FIAT [RATE FETCH ] currency=$currency") Timber.d("FIAT [RATE FETCH ] currency=$currency")
return api.getFiatRate(currency).rate return api.getFiatRate(currency).rate
} }
override suspend fun getSupportedCurrencies(): List<String> { override suspend fun getSupportedCurrencies(): List<String> {
Log.d(TAG, "FIAT [CURRENCIES] fetching supported currency list") Timber.d("FIAT [CURRENCIES] fetching supported currency list")
return api.getSupportedCurrencies() return api.getSupportedCurrencies()
} }
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
@@ -38,7 +38,7 @@ class LightningAddressStore(private val context: Context) {
.map { prefs -> .map { prefs ->
prefs[KEY_ADDRESS].also { address -> prefs[KEY_ADDRESS].also { address ->
_currentAddress = address _currentAddress = address
Log.d("LightningAddressStore", "LNADDR [CACHE ] read from cache: " + Timber.d("LNADDR [CACHE ] read from cache: " +
if (address != null) "\"$address\"" else "(empty — nothing cached yet)") } if (address != null) "\"$address\"" else "(empty — nothing cached yet)") }
} }
@@ -53,7 +53,7 @@ class LightningAddressStore(private val context: Context) {
val fetchedAt = prefs[KEY_FETCHED_AT] ?: 0L val fetchedAt = prefs[KEY_FETCHED_AT] ?: 0L
val age = System.currentTimeMillis() - fetchedAt val age = System.currentTimeMillis() - fetchedAt
val isStale = address == null || age > LNADDRESS_TTL_MS val isStale = address == null || age > LNADDRESS_TTL_MS
Log.d("LightningAddressStore", "LNADDR [TTL ] cached=${address != null} " + Timber.d("LNADDR [TTL ] cached=${address != null} " +
"age=${age / 1000}s ttl=${LNADDRESS_TTL_MS / 1000}s " + "age=${age / 1000}s ttl=${LNADDRESS_TTL_MS / 1000}s " +
if (isStale) "→ cache stale, will fetch from API" if (isStale) "→ cache stale, will fetch from API"
else "→ cache fresh, skipping API call") else "→ cache fresh, skipping API call")
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.GsonProvider import com.bitcointxoko.gudariwallet.api.GsonProvider
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.data.db.AppDatabase import com.bitcointxoko.gudariwallet.data.db.AppDatabase
@@ -34,13 +34,13 @@ class LnurlCacheRepository(context: Context) {
val ageMs = System.currentTimeMillis() - entity.savedAt val ageMs = System.currentTimeMillis() - entity.savedAt
if (ageMs > WalletConstants.LNURL_CACHE_TTL_MS) { if (ageMs > WalletConstants.LNURL_CACHE_TTL_MS) {
Log.d(TAG, "LNURL [CACHE STALE] $key (age ${ageMs / 1000}s)") Timber.d("LNURL [CACHE STALE] $key (age ${ageMs / 1000}s)")
evict(key) evict(key)
return null return null
} }
val response = deserialise(entity.responseJson) ?: run { val response = deserialise(entity.responseJson) ?: run {
Log.w(TAG, "LNURL [DESER ERR ] $key — removing corrupt entry") Timber.w("LNURL [DESER ERR ] $key — removing corrupt entry")
evict(key) evict(key)
return null return null
} }
@@ -48,11 +48,11 @@ class LnurlCacheRepository(context: Context) {
// LUD-06: disposable == null means treat as true — do not serve from cache // LUD-06: disposable == null means treat as true — do not serve from cache
val disposable = response.disposable val disposable = response.disposable
if (disposable == true) { if (disposable == true) {
Log.d(TAG, "LNURL [DISPOSABLE ] $key — not serving from cache (disposable=$disposable)") Timber.d("LNURL [DISPOSABLE ] $key — not serving from cache (disposable=$disposable)")
return null return null
} }
Log.d(TAG, "LNURL [CACHE HIT ] $key (age ${ageMs / 1000}s)") Timber.d("LNURL [CACHE HIT ] $key (age ${ageMs / 1000}s)")
return response return response
} }
@@ -63,7 +63,7 @@ class LnurlCacheRepository(context: Context) {
suspend fun put(key: String, response: LnurlScanResponse) { suspend fun put(key: String, response: LnurlScanResponse) {
val disposable = response.disposable val disposable = response.disposable
if ( disposable == true) { if ( disposable == true) {
Log.d(TAG, "LNURL [SKIP CACHE ] $key — disposable=$disposable, not caching") Timber.d("LNURL [SKIP CACHE ] $key — disposable=$disposable, not caching")
return return
} }
val entity = LnurlCacheEntity( val entity = LnurlCacheEntity(
@@ -74,9 +74,9 @@ class LnurlCacheRepository(context: Context) {
runCatching { runCatching {
dao.insert(entity) dao.insert(entity)
memCache[key] = entity memCache[key] = entity
Log.d(TAG, "LNURL [CACHE SET ] $key") Timber.d("LNURL [CACHE SET ] $key")
}.onFailure { }.onFailure {
Log.w(TAG, "LNURL [PERSIST ERR] ${it.message}") Timber.w("LNURL [PERSIST ERR] ${it.message}")
} }
} }
@@ -87,14 +87,14 @@ class LnurlCacheRepository(context: Context) {
*/ */
suspend fun invalidate(key: String) { suspend fun invalidate(key: String) {
evict(key) evict(key)
Log.d(TAG, "LNURL [INVALIDATED] $key") Timber.d("LNURL [INVALIDATED] $key")
} }
suspend fun clearAll() { suspend fun clearAll() {
memCache.clear() memCache.clear()
runCatching { dao.deleteAll() } runCatching { dao.deleteAll() }
.onFailure { Log.w(TAG, "LNURL [CLEAR ERR ] ${it.message}") } .onFailure { Timber.w("LNURL [CLEAR ERR ] ${it.message}") }
Log.d(TAG, "LNURL [CLEARED ] all entries") Timber.d("LNURL [CLEARED ] all entries")
} }
// ── Helpers ──────────────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────────────
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.data.db.AppDatabase import com.bitcointxoko.gudariwallet.data.db.AppDatabase
import com.bitcointxoko.gudariwallet.data.db.NodeAliasCacheEntity import com.bitcointxoko.gudariwallet.data.db.NodeAliasCacheEntity
import com.bitcointxoko.gudariwallet.util.WalletConstants import com.bitcointxoko.gudariwallet.util.WalletConstants
@@ -18,15 +18,15 @@ class NodeAliasCacheRepository(context: Context) {
*/ */
suspend fun get(pubkey: String): String? { suspend fun get(pubkey: String): String? {
val entry = dao.getByPubkey(pubkey) ?: run { val entry = dao.getByPubkey(pubkey) ?: run {
Log.d(TAG, "ALIAS [CACHE MISS ] $pubkey") Timber.d("ALIAS [CACHE MISS ] $pubkey")
return null return null
} }
val ageMs = System.currentTimeMillis() - entry.savedAt val ageMs = System.currentTimeMillis() - entry.savedAt
return if (ageMs < WalletConstants.ALIAS_CACHE_TTL_MS) { return if (ageMs < WalletConstants.ALIAS_CACHE_TTL_MS) {
Log.d(TAG, "ALIAS [CACHE HIT ] $pubkey\"${entry.alias}\" (age ${ageMs / 1000}s)") Timber.d("ALIAS [CACHE HIT ] $pubkey\"${entry.alias}\" (age ${ageMs / 1000}s)")
entry.alias entry.alias
} else { } else {
Log.d(TAG, "ALIAS [CACHE STALE] $pubkey (age ${ageMs / 1000}s) — will refetch") Timber.d("ALIAS [CACHE STALE] $pubkey (age ${ageMs / 1000}s) — will refetch")
null null
} }
} }
@@ -40,7 +40,7 @@ class NodeAliasCacheRepository(context: Context) {
savedAt = System.currentTimeMillis() savedAt = System.currentTimeMillis()
) )
) )
Log.d(TAG, "ALIAS [PERSIST ] $pubkey\"$alias\"") Timber.d("ALIAS [PERSIST ] $pubkey\"$alias\"")
} }
/** /**
@@ -52,7 +52,7 @@ class NodeAliasCacheRepository(context: Context) {
return dao.getAll() return dao.getAll()
.filter { now - it.savedAt < WalletConstants.ALIAS_PERSIST_TTL_MS } .filter { now - it.savedAt < WalletConstants.ALIAS_PERSIST_TTL_MS }
.associate { it.pubkey to it.alias } .associate { it.pubkey to it.alias }
.also { Log.d(TAG, "ALIAS [PERSIST ] Loaded ${it.size} non-stale entries from Room") } .also { Timber.d("ALIAS [PERSIST ] Loaded ${it.size} non-stale entries from Room") }
} }
/** Remove a single entry. */ /** Remove a single entry. */
@@ -63,6 +63,6 @@ class NodeAliasCacheRepository(context: Context) {
/** Wipe the entire alias cache — mirrors [clearPersistedCache]. */ /** Wipe the entire alias cache — mirrors [clearPersistedCache]. */
suspend fun clearAll() { suspend fun clearAll() {
dao.deleteAll() dao.deleteAll()
Log.d(TAG, "ALIAS [PERSIST ] Cache cleared") Timber.d("ALIAS [PERSIST ] Cache cleared")
} }
} }
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.PaymentDetailResponse import com.bitcointxoko.gudariwallet.api.PaymentDetailResponse
import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.db.AppDatabase import com.bitcointxoko.gudariwallet.data.db.AppDatabase
@@ -25,16 +25,16 @@ class PaymentCacheRepository(context: Context) {
/** Load the persisted first-page list. Returns empty list if nothing cached or cache is stale. */ /** Load the persisted first-page list. Returns empty list if nothing cached or cache is stale. */
suspend fun loadCachedPayments(): List<PaymentRecord> { suspend fun loadCachedPayments(): List<PaymentRecord> {
val savedAt = dao.getLatestSavedAt() ?: run { val savedAt = dao.getLatestSavedAt() ?: run {
Log.d(TAG, "PAYMENTS [CACHE MISS] no rows in db") Timber.d("PAYMENTS [CACHE MISS] no rows in db")
return emptyList() return emptyList()
} }
val ageMs = System.currentTimeMillis() - savedAt val ageMs = System.currentTimeMillis() - savedAt
if (ageMs > WalletConstants.PAYMENTS_CACHE_TTL_MS) { if (ageMs > WalletConstants.PAYMENTS_CACHE_TTL_MS) {
Log.d(TAG, "PAYMENTS [CACHE STALE] age ${ageMs / 1000}s — ignoring persisted list") Timber.d("PAYMENTS [CACHE STALE] age ${ageMs / 1000}s — ignoring persisted list")
return emptyList() return emptyList()
} }
return dao.getAll().map { it.toDomain() }.also { return dao.getAll().map { it.toDomain() }.also {
Log.d(TAG, "PAYMENTS [CACHE HIT ] loaded ${it.size} payments (age ${ageMs / 1000}s)") Timber.d("PAYMENTS [CACHE HIT ] loaded ${it.size} payments (age ${ageMs / 1000}s)")
} }
} }
@@ -44,9 +44,9 @@ class PaymentCacheRepository(context: Context) {
.map { it.toDomain() } .map { it.toDomain() }
.also { .also {
if (it.isNotEmpty()) if (it.isNotEmpty())
Log.d(TAG, "PAYMENTS [CACHE HIT ] offset=$offset got ${it.size} rows from Room") Timber.d("PAYMENTS [CACHE HIT ] offset=$offset got ${it.size} rows from Room")
else else
Log.d(TAG, "PAYMENTS [CACHE MISS] offset=$offset — no rows in Room") Timber.d("PAYMENTS [CACHE MISS] offset=$offset — no rows in Room")
} }
} }
@@ -59,9 +59,9 @@ class PaymentCacheRepository(context: Context) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
runCatching { runCatching {
dao.insertAll(payments.map { it.toEntity(now) }) dao.insertAll(payments.map { it.toEntity(now) })
Log.d(TAG, "PAYMENTS [MERGE ] upserted ${payments.size} payments (total: ${dao.count()})") Timber.d("PAYMENTS [MERGE ] upserted ${payments.size} payments (total: ${dao.count()})")
}.onFailure { }.onFailure {
Log.w(TAG, "PAYMENTS [MERGE ERR] ${it.message}") Timber.w("PAYMENTS [MERGE ERR] ${it.message}")
} }
} }
@@ -71,9 +71,9 @@ class PaymentCacheRepository(context: Context) {
runCatching { runCatching {
dao.deleteAll() dao.deleteAll()
dao.insertAll(payments.map { it.toEntity(now) }) dao.insertAll(payments.map { it.toEntity(now) })
Log.d(TAG, "PAYMENTS [PERSIST ] saved ${payments.size} payments") Timber.d("PAYMENTS [PERSIST ] saved ${payments.size} payments")
}.onFailure { }.onFailure {
Log.w(TAG, "PAYMENTS [PERSIST ] write failed: ${it.message}") Timber.w("PAYMENTS [PERSIST ] write failed: ${it.message}")
} }
} }
@@ -109,7 +109,7 @@ class PaymentCacheRepository(context: Context) {
minCreatedAt = filter.minCreatedAt, minCreatedAt = filter.minCreatedAt,
maxCreatedAt = filter.maxCreatedAt maxCreatedAt = filter.maxCreatedAt
).map { it.toDomain() }.also { ).map { it.toDomain() }.also {
Log.d(TAG, "PAYMENTS [QUERY ] q=$searchQuery status=$statusPattern " + Timber.d("PAYMENTS [QUERY ] q=$searchQuery status=$statusPattern " +
"amt=${filter.minAmountSat}${filter.maxAmountSat} " + "amt=${filter.minAmountSat}${filter.maxAmountSat} " +
"date=${filter.minCreatedAt}${filter.maxCreatedAt}${it.size} rows") "date=${filter.minCreatedAt}${filter.maxCreatedAt}${it.size} rows")
} }
@@ -120,26 +120,26 @@ class PaymentCacheRepository(context: Context) {
/** Returns a cached detail response, or null if not yet fetched this session. */ /** Returns a cached detail response, or null if not yet fetched this session. */
fun getCachedDetail(checkingId: String): PaymentDetailResponse? = fun getCachedDetail(checkingId: String): PaymentDetailResponse? =
detailCache[checkingId]?.also { detailCache[checkingId]?.also {
Log.d(TAG, "PAYMENTS [DETAIL HIT] $checkingId") Timber.d("PAYMENTS [DETAIL HIT] $checkingId")
} }
/** Store a detail response in the in-memory cache. */ /** Store a detail response in the in-memory cache. */
fun saveDetail(checkingId: String, detail: PaymentDetailResponse) { fun saveDetail(checkingId: String, detail: PaymentDetailResponse) {
detailCache[checkingId] = detail detailCache[checkingId] = detail
Log.d(TAG, "PAYMENTS [DETAIL SET] $checkingId") Timber.d("PAYMENTS [DETAIL SET] $checkingId")
} }
/** Upsert PaymentRecord to database. */ /** Upsert PaymentRecord to database. */
suspend fun upsertPayment(payment: PaymentRecord) { suspend fun upsertPayment(payment: PaymentRecord) {
val entity = payment.toEntity(savedAt = System.currentTimeMillis()) val entity = payment.toEntity(savedAt = System.currentTimeMillis())
dao.insert(entity) dao.insert(entity)
Log.d(TAG, "PAYMENTS [UPSERT ] ${payment.checkingId}") Timber.d("PAYMENTS [UPSERT ] ${payment.checkingId}")
} }
/** Check the DB for a stored payment record and return it as a detail response. */ /** Check the DB for a stored payment record and return it as a detail response. */
suspend fun getDetailFromDb(checkingId: String): PaymentDetailResponse? { suspend fun getDetailFromDb(checkingId: String): PaymentDetailResponse? {
val entity = dao.getByCheckingId(checkingId) ?: return null val entity = dao.getByCheckingId(checkingId) ?: return null
Log.d(TAG, "PAYMENTS [DETAIL DB ] $checkingId — found in Room") Timber.d("PAYMENTS [DETAIL DB ] $checkingId — found in Room")
return PaymentDetailResponse( return PaymentDetailResponse(
paid = entity.status == "success", paid = entity.status == "success",
details = entity.toDomain() details = entity.toDomain()
@@ -158,6 +158,6 @@ class PaymentCacheRepository(context: Context) {
suspend fun clearAll() { suspend fun clearAll() {
detailCache.clear() detailCache.clear()
dao.deleteAll() dao.deleteAll()
Log.d(TAG, "PAYMENTS [CLEARED ] cache wiped") Timber.d("PAYMENTS [CLEARED ] cache wiped")
} }
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.data.db package com.bitcointxoko.gudariwallet.data.db
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.GsonProvider.gson import com.bitcointxoko.gudariwallet.api.GsonProvider.gson
import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.db.PaymentRecordEntity import com.bitcointxoko.gudariwallet.data.db.PaymentRecordEntity
@@ -16,7 +16,7 @@ private fun parseCreatedAt(time: String): Long? {
java.time.OffsetDateTime.parse(time).toEpochSecond() java.time.OffsetDateTime.parse(time).toEpochSecond()
}.getOrNull() }.getOrNull()
if (result == null) { if (result == null) {
Log.w(TAG, "parseCreatedAt: could not parse time string \"$time\" — createdAt will be null") Timber.w("parseCreatedAt: could not parse time string \"$time\" — createdAt will be null")
} }
return result return result
} }
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.security package com.bitcointxoko.gudariwallet.security
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.security package com.bitcointxoko.gudariwallet.security
import android.util.Log import timber.log.Timber
import androidx.datastore.core.CorruptionException import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer import androidx.datastore.core.Serializer
import com.google.crypto.tink.StreamingAead import com.google.crypto.tink.StreamingAead
@@ -22,10 +22,10 @@ class CredentialsSerializer(
val result = Credentials.parseFrom( val result = Credentials.parseFrom(
streamingAead.newDecryptingStream(input, AAD) streamingAead.newDecryptingStream(input, AAD)
) )
Log.d(TAG, "Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}") Timber.d("Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}")
result result
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Decryption failed — DataStore may be corrupt or key rotated", e) Timber.e("Decryption failed — DataStore may be corrupt or key rotated", e)
throw CorruptionException("Cannot read/decrypt credentials DataStore", e) throw CorruptionException("Cannot read/decrypt credentials DataStore", e)
} }
} }
@@ -37,12 +37,12 @@ class CredentialsSerializer(
val encryptingStream = streamingAead.newEncryptingStream(output, AAD) val encryptingStream = streamingAead.newEncryptingStream(output, AAD)
t.writeTo(encryptingStream) // returns Int — discarded by Unit return type t.writeTo(encryptingStream) // returns Int — discarded by Unit return type
encryptingStream.close() encryptingStream.close()
Log.d(TAG, "Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " + Timber.d("Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " +
"hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, " + "hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, " +
"hasAdminKey: ${t.adminKey.isNotBlank()}, " + "hasAdminKey: ${t.adminKey.isNotBlank()}, " +
"hasBaseUrl: ${t.baseUrl.isNotBlank()}") "hasBaseUrl: ${t.baseUrl.isNotBlank()}")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Encryption/write failed", e) Timber.e("Encryption/write failed", e)
throw e throw e
} }
} }
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.security package com.bitcointxoko.gudariwallet.security
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.util.WalletConstants import com.bitcointxoko.gudariwallet.util.WalletConstants
import com.google.crypto.tink.StreamingAead import com.google.crypto.tink.StreamingAead
import com.google.crypto.tink.integration.android.AndroidKeysetManager import com.google.crypto.tink.integration.android.AndroidKeysetManager
@@ -21,7 +21,7 @@ object CredentialsTinkManager {
init { init {
// Register all StreamingAead primitives once at class load time // Register all StreamingAead primitives once at class load time
StreamingAeadConfig.register() StreamingAeadConfig.register()
Log.d(TAG, "StreamingAeadConfig registered") Timber.d("StreamingAeadConfig registered")
} }
fun getOrCreateStreamingAead(context: Context): StreamingAead { fun getOrCreateStreamingAead(context: Context): StreamingAead {
@@ -33,12 +33,12 @@ object CredentialsTinkManager {
.build() .build()
.keysetHandle .keysetHandle
Log.i(TAG, "Keyset loaded — key count: ${handle.size()}, primary key ID: ${handle.primary.id}") Timber.i("Keyset loaded — key count: ${handle.size()}, primary key ID: ${handle.primary.id}")
handle.getPrimitive(StreamingAead::class.java).also { handle.getPrimitive(StreamingAead::class.java).also {
Log.i(TAG, "StreamingAead primitive acquired successfully") Timber.i("StreamingAead primitive acquired successfully")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to initialise StreamingAead — Keystore may be unavailable", e) Timber.e("Failed to initialise StreamingAead — Keystore may be unavailable", e)
throw e throw e
} }
} }
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.security package com.bitcointxoko.gudariwallet.security
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -50,7 +50,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
invoiceKey: String, invoiceKey: String,
adminKey: String adminKey: String
) { ) {
Log.i(TAG, "Saving credentials — baseUrl length: ${baseUrl.length}, " + Timber.i("Saving credentials — baseUrl length: ${baseUrl.length}, " +
"invoiceKey length: ${invoiceKey.length}, adminKey length: ${adminKey.length}") "invoiceKey length: ${invoiceKey.length}, adminKey length: ${adminKey.length}")
dataStore.updateData { current -> dataStore.updateData { current ->
current.toBuilder() current.toBuilder()
@@ -59,7 +59,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
.setAdminKey(adminKey) .setAdminKey(adminKey)
.build() .build()
} }
Log.i(TAG, "Credentials saved and DataStore updated") Timber.i("Credentials saved and DataStore updated")
} }
override suspend fun saveBaseUrl(baseUrl: String) { override suspend fun saveBaseUrl(baseUrl: String) {
dataStore.updateData { current -> dataStore.updateData { current ->
@@ -67,7 +67,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
.setBaseUrl(baseUrl) .setBaseUrl(baseUrl)
.build() .build()
} }
Log.i(TAG, "BaseUrl saved and DataStore updated") Timber.i("BaseUrl saved and DataStore updated")
} }
override suspend fun saveInvoiceKey(invoiceKey: String) { override suspend fun saveInvoiceKey(invoiceKey: String) {
@@ -76,7 +76,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
.setInvoiceKey(invoiceKey) .setInvoiceKey(invoiceKey)
.build() .build()
} }
Log.i(TAG, "InvoiceKey saved and DataStore updated") Timber.i("InvoiceKey saved and DataStore updated")
} }
override suspend fun saveAdminKey(adminKey: String) { override suspend fun saveAdminKey(adminKey: String) {
@@ -85,7 +85,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
.setAdminKey(adminKey) .setAdminKey(adminKey)
.build() .build()
} }
Log.i(TAG, "AdminKey saved and DataStore updated") Timber.i("AdminKey saved and DataStore updated")
} }
override suspend fun setOnboarded() { override suspend fun setOnboarded() {
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.service package com.bitcointxoko.gudariwallet.service
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.NodeAliasClient as NodeAliasApiClient import com.bitcointxoko.gudariwallet.api.NodeAliasClient as NodeAliasApiClient
import com.bitcointxoko.gudariwallet.data.NodeAliasCacheRepository import com.bitcointxoko.gudariwallet.data.NodeAliasCacheRepository
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -26,14 +26,14 @@ class NodeAliasService(
suspend fun resolve(pubkey: String): String? { suspend fun resolve(pubkey: String): String? {
// 1. Room cache hit // 1. Room cache hit
repo.get(pubkey)?.let { repo.get(pubkey)?.let {
Log.d(TAG, "ALIAS [CACHE HIT ] $pubkey\"$it\"") Timber.d("ALIAS [CACHE HIT ] $pubkey\"$it\"")
return it return it
} }
// 2. Network via api/NodeAliasService (Retrofit, typed responses) // 2. Network via api/NodeAliasService (Retrofit, typed responses)
Log.d(TAG, "ALIAS [CACHE MISS] $pubkey — fetching from network") Timber.d("ALIAS [CACHE MISS] $pubkey — fetching from network")
val alias = apiClient.resolveAlias(pubkey) ?: run { val alias = apiClient.resolveAlias(pubkey) ?: run {
Log.w(TAG, "ALIAS [NOT FOUND ] $pubkey") Timber.w("ALIAS [NOT FOUND ] $pubkey")
return null return null
} }
@@ -43,7 +43,7 @@ class NodeAliasService(
// 4. Publish to StateFlow // 4. Publish to StateFlow
_aliases.value = _aliases.value + (pubkey to alias) _aliases.value = _aliases.value + (pubkey to alias)
Log.d(TAG, "ALIAS [FETCHED ] $pubkey\"$alias\"") Timber.d("ALIAS [FETCHED ] $pubkey\"$alias\"")
return alias return alias
} }
@@ -6,7 +6,7 @@ import android.content.Intent
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.GsonProvider import com.bitcointxoko.gudariwallet.api.GsonProvider
import com.bitcointxoko.gudariwallet.api.LNbitsClient import com.bitcointxoko.gudariwallet.api.LNbitsClient
import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.api.PaymentRecord
@@ -124,7 +124,7 @@ class WalletNotificationService : Service() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Log.d(TAG, "Service destroyed — closing WebSocket") Timber.d("Service destroyed — closing WebSocket")
reconnectJob?.cancel() reconnectJob?.cancel()
webSocket?.close(WalletConstants.WS_CLOSE_NORMAL, "Service stopped") webSocket?.close(WalletConstants.WS_CLOSE_NORMAL, "Service stopped")
webSocket = null webSocket = null
@@ -143,7 +143,7 @@ class WalletNotificationService : Service() {
val baseUrl = secrets.baseUrl() val baseUrl = secrets.baseUrl()
if (invoiceKey.isBlank() || baseUrl.isBlank()) { if (invoiceKey.isBlank() || baseUrl.isBlank()) {
Log.e(TAG, "Credentials not available — cannot open WebSocket") Timber.e("Credentials not available — cannot open WebSocket")
stopSelf() stopSelf()
return return
} }
@@ -153,36 +153,36 @@ class WalletNotificationService : Service() {
.replace("http://", "ws://") .replace("http://", "ws://")
.trimEnd('/') + "/api/v1/ws/${invoiceKey}" .trimEnd('/') + "/api/v1/ws/${invoiceKey}"
Log.d(TAG, "Opening WebSocket: $wsUrl") Timber.d("Opening WebSocket: $wsUrl")
val request = Request.Builder().url(wsUrl).build() val request = Request.Builder().url(wsUrl).build()
webSocket = LNbitsClient.httpClient.newWebSocket(request, object : WebSocketListener() { webSocket = LNbitsClient.httpClient.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(TAG, "WebSocket connected") Timber.d("WebSocket connected")
// Reset backoff on successful connection // Reset backoff on successful connection
reconnectAttempts = 0 reconnectAttempts = 0
currentBackoffMs = WalletConstants.WS_INITIAL_BACKOFF_MS currentBackoffMs = WalletConstants.WS_INITIAL_BACKOFF_MS
} }
override fun onMessage(webSocket: WebSocket, text: String) { override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "WebSocket message: $text") Timber.d("WebSocket message: $text")
handleMessage(text) handleMessage(text)
} }
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "WebSocket failure (attempt $reconnectAttempts): ${t.message}") Timber.e("WebSocket failure (attempt $reconnectAttempts): ${t.message}")
scheduleReconnect() scheduleReconnect()
} }
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket closing: $code $reason") Timber.d("WebSocket closing: $code $reason")
webSocket.close(WalletConstants.WS_CLOSE_NORMAL, null) webSocket.close(WalletConstants.WS_CLOSE_NORMAL, null)
} }
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket closed: $code $reason") Timber.d("WebSocket closed: $code $reason")
// Only reconnect on unexpected closes (not our own WS_CLOSE_NORMAL) // Only reconnect on unexpected closes (not our own WS_CLOSE_NORMAL)
if (code != WalletConstants.WS_CLOSE_NORMAL) { if (code != WalletConstants.WS_CLOSE_NORMAL) {
scheduleReconnect() scheduleReconnect()
@@ -257,7 +257,7 @@ class WalletNotificationService : Service() {
private fun scheduleReconnect() { private fun scheduleReconnect() {
if (reconnectAttempts >= WalletConstants.WS_MAX_ATTEMPTS) { if (reconnectAttempts >= WalletConstants.WS_MAX_ATTEMPTS) {
Log.e(TAG, "WebSocket gave up after ${WalletConstants.WS_MAX_ATTEMPTS} attempts — service will wait for next start") Timber.e("WebSocket gave up after ${WalletConstants.WS_MAX_ATTEMPTS} attempts — service will wait for next start")
// Do not stop the service — it will reconnect on next onStartCommand // Do not stop the service — it will reconnect on next onStartCommand
// (e.g. when the app is brought to foreground and MainActivity calls start()) // (e.g. when the app is brought to foreground and MainActivity calls start())
return return
@@ -267,7 +267,7 @@ class WalletNotificationService : Service() {
reconnectAttempts++ reconnectAttempts++
currentBackoffMs = (currentBackoffMs * 2).coerceAtMost(WalletConstants.WS_MAX_BACKOFF_MS) currentBackoffMs = (currentBackoffMs * 2).coerceAtMost(WalletConstants.WS_MAX_BACKOFF_MS)
Log.d(TAG, "Reconnecting in ${backoff}ms (attempt $reconnectAttempts of ${WalletConstants.WS_MAX_ATTEMPTS})") Timber.d("Reconnecting in ${backoff}ms (attempt $reconnectAttempts of ${WalletConstants.WS_MAX_ATTEMPTS})")
reconnectJob?.cancel() reconnectJob?.cancel()
reconnectJob = serviceScope.launch { reconnectJob = serviceScope.launch {
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.sync package com.bitcointxoko.gudariwallet.sync
import android.content.Context import android.content.Context
import android.util.Log import timber.log.Timber
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
@@ -29,11 +29,11 @@ class HistoricalSyncWorker(
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
// Guard: already completed — safe if WorkManager ever retries a success // Guard: already completed — safe if WorkManager ever retries a success
if (syncStore.isComplete()) { if (syncStore.isComplete()) {
Log.d(TAG, "Already complete — skipping") Timber.d("Already complete — skipping")
return Result.success() return Result.success()
} }
Log.d(TAG, "Starting historical payment sync") Timber.d("Starting historical payment sync")
var offset = 0 var offset = 0
var total = -1 var total = -1
@@ -41,14 +41,14 @@ class HistoricalSyncWorker(
val page = runCatching { val page = runCatching {
repo.getPaymentsPaginated(offset = offset, limit = PAGE_SIZE) repo.getPaymentsPaginated(offset = offset, limit = PAGE_SIZE)
}.getOrElse { e -> }.getOrElse { e ->
Log.w(TAG, "Network error on offset=$offset: ${e.message} — scheduling retry") Timber.w("Network error on offset=$offset: ${e.message} — scheduling retry")
return Result.retry() // WorkManager applies exponential backoff automatically return Result.retry() // WorkManager applies exponential backoff automatically
} }
// First page: learn the real total from the server // First page: learn the real total from the server
if (total == -1) { if (total == -1) {
total = page.total total = page.total
Log.d(TAG, "Server reports $total total payments") Timber.d("Server reports $total total payments")
if (total == 0) { if (total == 0) {
syncStore.markComplete() syncStore.markComplete()
return Result.success() return Result.success()
@@ -59,7 +59,7 @@ class HistoricalSyncWorker(
paymentCache.mergePayments(page.data) paymentCache.mergePayments(page.data)
offset += page.data.size offset += page.data.size
Log.d(TAG, "Progress: $offset / $total") Timber.d("Progress: $offset / $total")
setProgress(workDataOf( setProgress(workDataOf(
KEY_FETCHED to offset, KEY_FETCHED to offset,
KEY_TOTAL to total KEY_TOTAL to total
@@ -70,7 +70,7 @@ class HistoricalSyncWorker(
} }
syncStore.markComplete() syncStore.markComplete()
Log.d(TAG, "Historical sync complete — $offset payments stored") Timber.d("Historical sync complete — $offset payments stored")
NotificationHelper.notifySyncComplete(applicationContext, offset) NotificationHelper.notifySyncComplete(applicationContext, offset)
return Result.success() return Result.success()
} }
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.ui package com.bitcointxoko.gudariwallet.ui
import android.content.ClipboardManager import android.content.ClipboardManager
import android.util.Log import timber.log.Timber
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@@ -164,7 +164,7 @@ fun WalletScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
vm.navigateToReceive.collect { vm.navigateToReceive.collect {
Log.d("WalletScreen", "LNURL-WITHDRAW [NAV RECEIVED] switching to Receive tab") Timber.d("LNURL-WITHDRAW [NAV RECEIVED] switching to Receive tab")
navController.navigate(TabItem.Receive.route) { navController.navigate(TabItem.Receive.route) {
popUpTo(navController.graph.findStartDestination().id) { popUpTo(navController.graph.findStartDestination().id) {
saveState = true saveState = true
@@ -178,7 +178,7 @@ fun WalletScreen(
val intentUri = pendingPaymentUri.value val intentUri = pendingPaymentUri.value
LaunchedEffect(intentUri) { LaunchedEffect(intentUri) {
if (intentUri != null) { if (intentUri != null) {
Log.d("WalletScreen", "INTENT [URI] deep link received: $intentUri") Timber.d("INTENT [URI] deep link received: $intentUri")
pendingPaymentUri.value = null pendingPaymentUri.value = null
vm.scan(intentUri, sendStrings) vm.scan(intentUri, sendStrings)
navController.navigate(TabItem.Send.route) { navController.navigate(TabItem.Send.route) {
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui package com.bitcointxoko.gudariwallet.ui
import android.util.Log import timber.log.Timber
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
@@ -144,21 +144,21 @@ class WalletViewModel(
// via SharingStarted.Eagerly — this coroutine only does the network refresh. // via SharingStarted.Eagerly — this coroutine only does the network refresh.
viewModelScope.launch { viewModelScope.launch {
if (!lnAddressStore.isCacheStale()) { if (!lnAddressStore.isCacheStale()) {
Log.d(TAG, "LNADDR [TTL ] cache is fresh — skipping network fetch") Timber.d("LNADDR [TTL ] cache is fresh — skipping network fetch")
return@launch return@launch
} }
runCatching { repo.getLightningAddress() } runCatching { repo.getLightningAddress() }
.onSuccess { address -> .onSuccess { address ->
if (address != null) { if (address != null) {
Log.d(TAG, "LNADDR [FETCH OK ] fetched from API: \"$address\" — updating cache") Timber.d("LNADDR [FETCH OK ] fetched from API: \"$address\" — updating cache")
lnAddressStore.setLightningAddress(address) lnAddressStore.setLightningAddress(address)
// StateFlow updates automatically via the DataStore flow // StateFlow updates automatically via the DataStore flow
} else { } else {
Log.d(TAG, "LNADDR [FETCH OK ] fetched from API: no pay link / username configured") Timber.d("LNADDR [FETCH OK ] fetched from API: no pay link / username configured")
} }
} }
.onFailure { e -> .onFailure { e ->
Log.w(TAG, "LNADDR [FETCH ERR] API call failed (${e.message}) — falling back to cached value") Timber.w("LNADDR [FETCH ERR] API call failed (${e.message}) — falling back to cached value")
// Nothing to do — DataStore flow already holds the last good value // Nothing to do — DataStore flow already holds the last good value
} }
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.balance package com.bitcointxoko.gudariwallet.ui.balance
import android.util.Log import timber.log.Timber
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
@@ -45,7 +45,7 @@ class BalanceViewModel(
val savedAt = balancePrefs.getBalanceSavedAt() val savedAt = balancePrefs.getBalanceSavedAt()
val ageMs = System.currentTimeMillis() - savedAt val ageMs = System.currentTimeMillis() - savedAt
if (savedSats >= 0 && ageMs < WalletConstants.BALANCE_CACHE_TTL_MS) { if (savedSats >= 0 && ageMs < WalletConstants.BALANCE_CACHE_TTL_MS) {
Log.d(TAG, "BALANCE [CACHE HIT ] $savedSats sats (age ${ageMs / 1000}s) — skipping network fetch") Timber.d("BALANCE [CACHE HIT ] $savedSats sats (age ${ageMs / 1000}s) — skipping network fetch")
_balanceState.value = BalanceState.Success( _balanceState.value = BalanceState.Success(
sats = savedSats, sats = savedSats,
isRefreshing = false, isRefreshing = false,
@@ -53,9 +53,9 @@ class BalanceViewModel(
) )
} else { } else {
if (savedSats >= 0) { if (savedSats >= 0) {
Log.d(TAG, "BALANCE [CACHE STALE] $savedSats sats, age ${ageMs / 1000}s > TTL — cold load") Timber.d("BALANCE [CACHE STALE] $savedSats sats, age ${ageMs / 1000}s > TTL — cold load")
} else { } else {
Log.d(TAG, "BALANCE [CACHE MISS ] No persisted balance — cold load") Timber.d("BALANCE [CACHE MISS ] No persisted balance — cold load")
} }
_balanceState.value = BalanceState.Loading _balanceState.value = BalanceState.Loading
refreshBalance() refreshBalance()
@@ -67,7 +67,7 @@ class BalanceViewModel(
fun refreshBalance() { fun refreshBalance() {
val current = _balanceState.value val current = _balanceState.value
if (current is BalanceState.Success && !current.isRefreshing) { if (current is BalanceState.Success && !current.isRefreshing) {
Log.d(TAG, "BALANCE [REFRESH ] Manual refresh triggered (current: ${current.sats} sats)") Timber.d("BALANCE [REFRESH ] Manual refresh triggered (current: ${current.sats} sats)")
_balanceState.value = current.copy(isRefreshing = true) _balanceState.value = current.copy(isRefreshing = true)
} }
@@ -77,9 +77,9 @@ class BalanceViewModel(
val sats = balance.sats val sats = balance.sats
val prev = (_balanceState.value as? BalanceState.Success)?.sats val prev = (_balanceState.value as? BalanceState.Success)?.sats
when { when {
prev == null -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — cold load complete") prev == null -> Timber.d("BALANCE [NETWORK ] $sats sats — cold load complete")
prev == sats -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — matches cache, no change") prev == sats -> Timber.d("BALANCE [NETWORK ] $sats sats — matches cache, no change")
else -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})") else -> Timber.d("BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})")
} }
_balanceState.value = BalanceState.Success( _balanceState.value = BalanceState.Success(
sats = sats, sats = sats,
@@ -91,13 +91,13 @@ class BalanceViewModel(
onBalanceRefreshed?.invoke() onBalanceRefreshed?.invoke()
} }
.onFailure { e -> .onFailure { e ->
Log.w(TAG, "BALANCE [NET ERROR ] ${e.message}") Timber.w("BALANCE [NET ERROR ] ${e.message}")
val staleSats = (_balanceState.value as? BalanceState.Success)?.sats val staleSats = (_balanceState.value as? BalanceState.Success)?.sats
?: balancePrefs.getSavedBalanceSats().takeIf { it >= 0 } ?: balancePrefs.getSavedBalanceSats().takeIf { it >= 0 }
if (staleSats != null) { if (staleSats != null) {
Log.w(TAG, "BALANCE [NET ERROR ] Falling back to stale value: $staleSats sats") Timber.w("BALANCE [NET ERROR ] Falling back to stale value: $staleSats sats")
} else { } else {
Log.w(TAG, "BALANCE [NET ERROR ] No stale value available — showing error state") Timber.w("BALANCE [NET ERROR ] No stale value available — showing error state")
} }
_balanceState.value = BalanceState.Error( _balanceState.value = BalanceState.Error(
message = e.message ?: "Failed to load balance", message = e.message ?: "Failed to load balance",
@@ -115,11 +115,11 @@ class BalanceViewModel(
private fun observePaymentEvents() { private fun observePaymentEvents() {
viewModelScope.launch { viewModelScope.launch {
WalletNotificationService.paymentEvents.collect { event -> WalletNotificationService.paymentEvents.collect { event ->
Log.d(TAG, "BALANCE [WS EVENT ] ${if (event.isOutgoing) "SENT" else "RECEIVED"} ${event.amountSats} sats") Timber.d("BALANCE [WS EVENT ] ${if (event.isOutgoing) "SENT" else "RECEIVED"} ${event.amountSats} sats")
val newSats = if (event.walletBalance != null) { val newSats = if (event.walletBalance != null) {
// Authoritative balance from the server — no delta math needed // Authoritative balance from the server — no delta math needed
Log.d(TAG, "BALANCE [WS BALANCE] ${event.walletBalance} sats (from server)") Timber.d("BALANCE [WS BALANCE] ${event.walletBalance} sats (from server)")
event.walletBalance event.walletBalance
} else { } else {
// Fallback: server didn't send balance, apply delta optimistically // Fallback: server didn't send balance, apply delta optimistically
@@ -127,7 +127,7 @@ class BalanceViewModel(
if (current is BalanceState.Success) { if (current is BalanceState.Success) {
val delta = if (event.isOutgoing) -event.amountSats else event.amountSats val delta = if (event.isOutgoing) -event.amountSats else event.amountSats
val sats = (current.sats + delta).coerceAtLeast(0L) val sats = (current.sats + delta).coerceAtLeast(0L)
Log.d(TAG, "BALANCE [WS DELTA ] ${current.sats} ${if (delta >= 0) "+" else ""}$delta = $sats sats (optimistic fallback)") Timber.d("BALANCE [WS DELTA ] ${current.sats} ${if (delta >= 0) "+" else ""}$delta = $sats sats (optimistic fallback)")
sats sats
} else null } else null
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.clipboard package com.bitcointxoko.gudariwallet.ui.clipboard
import android.util.Log import timber.log.Timber
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.bitcointxoko.gudariwallet.ui.ClipboardOfferState import com.bitcointxoko.gudariwallet.ui.ClipboardOfferState
import com.bitcointxoko.gudariwallet.util.SendInputDetector import com.bitcointxoko.gudariwallet.util.SendInputDetector
@@ -23,7 +23,7 @@ class ClipboardViewModel(
val clipboardOfferState: StateFlow<ClipboardOfferState> = _clipboardOfferState val clipboardOfferState: StateFlow<ClipboardOfferState> = _clipboardOfferState
fun checkClipboard(text: String?) { fun checkClipboard(text: String?) {
Log.d(TAG, "checkClipboard called with: $text") Timber.d("checkClipboard called with: $text")
val trimmed = text?.trim().takeIf { !it.isNullOrBlank() } ?: return val trimmed = text?.trim().takeIf { !it.isNullOrBlank() } ?: return
// ── Deduplicate: don't re-offer the same string twice ───────────────── // ── Deduplicate: don't re-offer the same string twice ─────────────────
@@ -32,7 +32,7 @@ class ClipboardViewModel(
// ── Guard: ignore our own invoice ────────────────────────────────────── // ── Guard: ignore our own invoice ──────────────────────────────────────
val invoice = ownInvoice() val invoice = ownInvoice()
if (invoice != null && trimmed.equals(invoice, ignoreCase = true)) { if (invoice != null && trimmed.equals(invoice, ignoreCase = true)) {
Log.d(TAG, "Skipping own invoice") Timber.d("Skipping own invoice")
lastOfferedClipboard = trimmed // mark as seen so re-focus doesn't re-trigger lastOfferedClipboard = trimmed // mark as seen so re-focus doesn't re-trigger
return return
} }
@@ -40,7 +40,7 @@ class ClipboardViewModel(
// ── Guard: ignore our own lightning address ──────────────────────────── // ── Guard: ignore our own lightning address ────────────────────────────
val address = ownAddress() val address = ownAddress()
if (address != null && trimmed.equals(address, ignoreCase = true)) { if (address != null && trimmed.equals(address, ignoreCase = true)) {
Log.d(TAG, "Skipping own lightning address") Timber.d("Skipping own lightning address")
lastOfferedClipboard = trimmed lastOfferedClipboard = trimmed
return return
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.fiat package com.bitcointxoko.gudariwallet.ui.fiat
import android.util.Log import timber.log.Timber
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
@@ -57,7 +57,7 @@ class FiatViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
_selectedCurrency.value = balancePrefs.getSelectedCurrency() _selectedCurrency.value = balancePrefs.getSelectedCurrency()
Log.d(TAG, "FIAT [CURRENCY ] loaded from DataStore: ${_selectedCurrency.value ?: "none"}") Timber.d("FIAT [CURRENCY ] loaded from DataStore: ${_selectedCurrency.value ?: "none"}")
balanceVm.balanceState.collect { state -> balanceVm.balanceState.collect { state ->
if (state is BalanceState.Success && !state.isRefreshing) { if (state is BalanceState.Success && !state.isRefreshing) {
fetchAndApplyFiatRate() fetchAndApplyFiatRate()
@@ -72,7 +72,7 @@ class FiatViewModel(
viewModelScope.launch { viewModelScope.launch {
balancePrefs.setSelectedCurrency(currency) balancePrefs.setSelectedCurrency(currency)
fiatCache.invalidateRate() fiatCache.invalidateRate()
Log.d(TAG, "FIAT [CURRENCY ] changed to ${currency ?: "none"} — cache invalidated") Timber.d("FIAT [CURRENCY ] changed to ${currency ?: "none"} — cache invalidated")
if (currency != null) { if (currency != null) {
viewModelScope.launch { fetchAndApplyFiatRate() } viewModelScope.launch { fetchAndApplyFiatRate() }
} else { } else {
@@ -109,8 +109,8 @@ class FiatViewModel(
?: run { ?: run {
runCatching { repo.getFiatRate(currency) } runCatching { repo.getFiatRate(currency) }
.getOrElse { e -> .getOrElse { e ->
Log.w(TAG, "FIAT [RATE ERROR ] $currency${e.message}") Timber.w("FIAT [RATE ERROR ] $currency${e.message}")
Log.w(TAG, "FIAT [RATE MISS ] $currency — no fallback available, suppressing fiat display") Timber.w("FIAT [RATE MISS ] $currency — no fallback available, suppressing fiat display")
return return
} }
.also { fresh -> fiatCache.putRate(currency, fresh) } .also { fresh -> fiatCache.putRate(currency, fresh) }
@@ -118,7 +118,7 @@ class FiatViewModel(
val currentSats = (balanceVm.balanceState.value as? BalanceState.Success)?.sats ?: return val currentSats = (balanceVm.balanceState.value as? BalanceState.Success)?.sats ?: return
val fiat = satsToFiat(currentSats, rate) val fiat = satsToFiat(currentSats, rate)
Log.d(TAG, "FIAT [APPLY ] $currentSats sats ÷ $rate sats/$currency = $fiat $currency") Timber.d("FIAT [APPLY ] $currentSats sats ÷ $rate sats/$currency = $fiat $currency")
balanceVm.applyFiat(fiat, currency) balanceVm.applyFiat(fiat, currency)
} }
@@ -131,7 +131,7 @@ class FiatViewModel(
private suspend fun fetchAndCacheCurrencies(): List<String> { private suspend fun fetchAndCacheCurrencies(): List<String> {
return runCatching { repo.getSupportedCurrencies() } return runCatching { repo.getSupportedCurrencies() }
.getOrElse { e -> .getOrElse { e ->
Log.w(TAG, "FIAT [CURRENCIES] network fetch failed — ${e.message}") Timber.w("FIAT [CURRENCIES] network fetch failed — ${e.message}")
emptyList() emptyList()
} }
.also { list -> fiatCache.putCurrencies(list) } .also { list -> fiatCache.putCurrencies(list) }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.history package com.bitcointxoko.gudariwallet.ui.history
import android.util.Log import timber.log.Timber
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -161,10 +161,10 @@ class HistoryViewModel(
) { _, f -> f } ) { _, f -> f }
.collect { f -> .collect { f ->
if (!f.needsRoomQuery()) { if (!f.needsRoomQuery()) {
Log.d(TAG, "FILTER [IN-MEMORY] filter=default → using cached state") Timber.d("FILTER [IN-MEMORY] filter=default → using cached state")
_roomResults.value = null _roomResults.value = null
} else { } else {
Log.d(TAG, "FILTER [ROOM ] q=\"${f.searchQuery}\" " + Timber.d("FILTER [ROOM ] q=\"${f.searchQuery}\" " +
"statuses=${f.statuses} dir=${f.direction} types=${f.types} " + "statuses=${f.statuses} dir=${f.direction} types=${f.types} " +
"amt=${f.minAmountSat}${f.maxAmountSat} " + "amt=${f.minAmountSat}${f.maxAmountSat} " +
"date=${f.minCreatedAt}${f.maxCreatedAt}") "date=${f.minCreatedAt}${f.maxCreatedAt}")
@@ -174,7 +174,7 @@ class HistoryViewModel(
} }
viewModelScope.launch { viewModelScope.launch {
WalletNotificationService.paymentEvents.collect { event -> WalletNotificationService.paymentEvents.collect { event ->
Log.d(TAG, "PAYMENTS [WS EVENT ] incoming event ${event.paymentHash} — refreshing list") Timber.d("PAYMENTS [WS EVENT ] incoming event ${event.paymentHash} — refreshing list")
syncManager.refresh() syncManager.refresh()
} }
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.history package com.bitcointxoko.gudariwallet.ui.history
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -22,15 +22,15 @@ class PaymentDetailDelegate(
fun load(checkingId: String, record: PaymentRecord? = null) { fun load(checkingId: String, record: PaymentRecord? = null) {
paymentCache.getCachedDetail(checkingId)?.let { paymentCache.getCachedDetail(checkingId)?.let {
Log.d(TAG, "DETAIL [HIT] $checkingId — instant from cache") Timber.d("DETAIL [HIT] $checkingId — instant from cache")
_state.value = DetailState.Success(it) _state.value = DetailState.Success(it)
return return
} }
_state.value = if (record != null) { _state.value = if (record != null) {
Log.d(TAG, "DETAIL [PAR] $checkingId — partial from PaymentRecord") Timber.d("DETAIL [PAR] $checkingId — partial from PaymentRecord")
DetailState.Partial(record) DetailState.Partial(record)
} else { } else {
Log.d(TAG, "DETAIL [LOD] $checkingId — no record, showing spinner") Timber.d("DETAIL [LOD] $checkingId — no record, showing spinner")
DetailState.Loading DetailState.Loading
} }
scope.launch { scope.launch {
@@ -38,17 +38,17 @@ class PaymentDetailDelegate(
if (fromDb != null) { if (fromDb != null) {
paymentCache.saveDetail(checkingId, fromDb) paymentCache.saveDetail(checkingId, fromDb)
_state.value = DetailState.Success(fromDb) _state.value = DetailState.Success(fromDb)
Log.d(TAG, "DETAIL [DB] $checkingId — served from Room") Timber.d("DETAIL [DB] $checkingId — served from Room")
return@launch return@launch
} }
runCatching { repo.getPaymentDetail(checkingId) } runCatching { repo.getPaymentDetail(checkingId) }
.onSuccess { detail -> .onSuccess { detail ->
Log.d(TAG, "DETAIL [NET] $checkingId — fetched from network") Timber.d("DETAIL [NET] $checkingId — fetched from network")
paymentCache.saveDetail(checkingId, detail) paymentCache.saveDetail(checkingId, detail)
_state.value = DetailState.Success(detail) _state.value = DetailState.Success(detail)
} }
.onFailure { e -> .onFailure { e ->
Log.w(TAG, "DETAIL [ERR] $checkingId${e.message}") Timber.w("DETAIL [ERR] $checkingId${e.message}")
if (_state.value !is DetailState.Partial) { if (_state.value !is DetailState.Partial) {
_state.value = DetailState.Error(e.message ?: "Failed to load payment") _state.value = DetailState.Error(e.message ?: "Failed to load payment")
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.history package com.bitcointxoko.gudariwallet.ui.history
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
@@ -21,7 +21,7 @@ class PaymentEnricher(
val candidates = payments.filter { it.isOutgoing } val candidates = payments.filter { it.isOutgoing }
if (candidates.isEmpty()) return if (candidates.isEmpty()) return
val toEnrich = candidates.filter { !it.bolt11.isNullOrBlank() && it.pubkey == null } val toEnrich = candidates.filter { !it.bolt11.isNullOrBlank() && it.pubkey == null }
Log.d(TAG, "ENRICH [START ] ${toEnrich.size} payments to enrich " + Timber.d("ENRICH [START ] ${toEnrich.size} payments to enrich " +
"(${candidates.size - toEnrich.size} already enriched or no bolt11)") "(${candidates.size - toEnrich.size} already enriched or no bolt11)")
if (toEnrich.isEmpty()) return if (toEnrich.isEmpty()) return
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.history package com.bitcointxoko.gudariwallet.ui.history
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -37,7 +37,7 @@ class PaymentSyncManager(
limit = WalletConstants.PAYMENTS_PAGE_SIZE limit = WalletConstants.PAYMENTS_PAGE_SIZE
) )
if (cached.isNotEmpty()) { if (cached.isNotEmpty()) {
Log.d(TAG, "PAYMENTS [INIT ] Showing ${cached.size} cached payments immediately") Timber.d("PAYMENTS [INIT ] Showing ${cached.size} cached payments immediately")
currentOffset = cached.size currentOffset = cached.size
mutableState.value = HistoryState.Success( mutableState.value = HistoryState.Success(
payments = cached.distinctBy { it.paymentHash }, payments = cached.distinctBy { it.paymentHash },
@@ -45,7 +45,7 @@ class PaymentSyncManager(
isRefreshing = true isRefreshing = true
) )
} else { } else {
Log.d(TAG, "PAYMENTS [INIT ] No cache — cold load") Timber.d("PAYMENTS [INIT ] No cache — cold load")
} }
syncNewPayments() syncNewPayments()
} }
@@ -78,7 +78,7 @@ class PaymentSyncManager(
limit = WalletConstants.PAYMENTS_PAGE_SIZE limit = WalletConstants.PAYMENTS_PAGE_SIZE
) )
if (fromDb.size == WalletConstants.PAYMENTS_PAGE_SIZE) { if (fromDb.size == WalletConstants.PAYMENTS_PAGE_SIZE) {
Log.d(TAG, "PAYMENTS [MORE DB ] offset=$currentOffset — served ${fromDb.size} rows from Room") Timber.d("PAYMENTS [MORE DB ] offset=$currentOffset — served ${fromDb.size} rows from Room")
val deduplicated = (current.payments + fromDb).distinctBy { it.paymentHash } val deduplicated = (current.payments + fromDb).distinctBy { it.paymentHash }
currentOffset += fromDb.size currentOffset += fromDb.size
val newState = HistoryState.Success( val newState = HistoryState.Success(
@@ -91,7 +91,7 @@ class PaymentSyncManager(
return@launch return@launch
} }
Log.d(TAG, "PAYMENTS [MORE NET ] offset=$currentOffset — DB has ${fromDb.size}, fetching from network") Timber.d("PAYMENTS [MORE NET ] offset=$currentOffset — DB has ${fromDb.size}, fetching from network")
runCatching { runCatching {
repo.getPayments(offset = currentOffset, limit = WalletConstants.PAYMENTS_PAGE_SIZE) repo.getPayments(offset = currentOffset, limit = WalletConstants.PAYMENTS_PAGE_SIZE)
}.onSuccess { newPage -> }.onSuccess { newPage ->
@@ -106,7 +106,7 @@ class PaymentSyncManager(
mutableState.value = newState mutableState.value = newState
onNewPayments(newState.payments) onNewPayments(newState.payments)
}.onFailure { e -> }.onFailure { e ->
Log.w(TAG, "PAYMENTS [MORE ERR ] ${e.message}") Timber.w("PAYMENTS [MORE ERR ] ${e.message}")
mutableState.value = current.copy(isRefreshing = false) mutableState.value = current.copy(isRefreshing = false)
} }
} }
@@ -119,11 +119,11 @@ class PaymentSyncManager(
var totalNew = 0 var totalNew = 0
var pagesChecked = 0 var pagesChecked = 0
Log.d(TAG, "PAYMENTS [SYNC START] scanning for new payments (cap=$maxPayments)") Timber.d("PAYMENTS [SYNC START] scanning for new payments (cap=$maxPayments)")
while (true) { while (true) {
if (totalNew >= maxPayments) { if (totalNew >= maxPayments) {
Log.d(TAG, "PAYMENTS [SYNC CAP ] reached $maxPayments payment limit — stopping early") Timber.d("PAYMENTS [SYNC CAP ] reached $maxPayments payment limit — stopping early")
break break
} }
@@ -132,10 +132,10 @@ class PaymentSyncManager(
}.getOrElse { e -> }.getOrElse { e ->
val current = mutableState.value val current = mutableState.value
if (current is HistoryState.Success) { if (current is HistoryState.Success) {
Log.w(TAG, "PAYMENTS [SYNC ERR ] ${e.message} — keeping cached list") Timber.w("PAYMENTS [SYNC ERR ] ${e.message} — keeping cached list")
mutableState.value = current.copy(isRefreshing = false) mutableState.value = current.copy(isRefreshing = false)
} else { } else {
Log.w(TAG, "PAYMENTS [SYNC ERR ] ${e.message} — no cache to fall back on") Timber.w("PAYMENTS [SYNC ERR ] ${e.message} — no cache to fall back on")
mutableState.value = HistoryState.Error(e.message ?: "Failed to load payments") mutableState.value = HistoryState.Error(e.message ?: "Failed to load payments")
} }
return return
@@ -152,14 +152,14 @@ class PaymentSyncManager(
paymentCache.mergePayments(toMerge) paymentCache.mergePayments(toMerge)
totalNew += toMerge.size totalNew += toMerge.size
val withTimestamp = toMerge.count { it.time.toLongOrNull() != null } val withTimestamp = toMerge.count { it.time.toLongOrNull() != null }
Log.d(TAG, "PAYMENTS [SYNC PAGE ] page $pagesChecked${toMerge.size} new payments merged ($withTimestamp with numeric timestamp)") Timber.d("PAYMENTS [SYNC PAGE ] page $pagesChecked${toMerge.size} new payments merged ($withTimestamp with numeric timestamp)")
} }
val reachedOverlap = overlapIndex != -1 val reachedOverlap = overlapIndex != -1
val reachedEnd = page.size < WalletConstants.PAYMENTS_PAGE_SIZE val reachedEnd = page.size < WalletConstants.PAYMENTS_PAGE_SIZE
if (reachedOverlap || reachedEnd) { if (reachedOverlap || reachedEnd) {
Log.d(TAG, "PAYMENTS [SYNC DONE ] $totalNew new payment(s) across $pagesChecked page(s) — " + Timber.d("PAYMENTS [SYNC DONE ] $totalNew new payment(s) across $pagesChecked page(s) — " +
if (reachedOverlap) "stopped at known record" else "reached end of history") if (reachedOverlap) "stopped at known record" else "reached end of history")
break break
} }
@@ -2,7 +2,7 @@ package com.bitcointxoko.gudariwallet.ui.nfc
import android.nfc.cardemulation.HostApduService import android.nfc.cardemulation.HostApduService
import android.os.Bundle import android.os.Bundle
import android.util.Log import timber.log.Timber
/** /**
* HCE service that emulates an NFC Forum Type 4 Tag containing an NDEF * HCE service that emulates an NFC Forum Type 4 Tag containing an NDEF
@@ -71,7 +71,7 @@ class NdefHceService : HostApduService() {
private var selectedFileId: ByteArray? = null private var selectedFileId: ByteArray? = null
override fun processCommandApdu(apdu: ByteArray, extras: Bundle?): ByteArray { override fun processCommandApdu(apdu: ByteArray, extras: Bundle?): ByteArray {
Log.d(TAG, "APDU IN: ${apdu.toHex()}") Timber.d("APDU IN: ${apdu.toHex()}")
if (apdu.size < 4) return SW_UNKNOWN if (apdu.size < 4) return SW_UNKNOWN
@@ -85,7 +85,7 @@ class NdefHceService : HostApduService() {
ins == 0xB0.toByte() -> handleReadBinary(apdu) ins == 0xB0.toByte() -> handleReadBinary(apdu)
else -> SW_UNKNOWN else -> SW_UNKNOWN
}.also { Log.d(TAG, "APDU OUT: ${it.toHex()}") } }.also { Timber.d("APDU OUT: ${it.toHex()}") }
} }
private fun handleSelect(apdu: ByteArray): ByteArray { private fun handleSelect(apdu: ByteArray): ByteArray {
@@ -97,21 +97,21 @@ class NdefHceService : HostApduService() {
return when { return when {
data.contentEquals(NDEF_AID) -> { data.contentEquals(NDEF_AID) -> {
selectedFileId = null // app selected, no file yet selectedFileId = null // app selected, no file yet
Log.d(TAG, "SELECT NDEF Application OK") Timber.d("SELECT NDEF Application OK")
SW_OK SW_OK
} }
data.contentEquals(CC_FILE_ID) -> { data.contentEquals(CC_FILE_ID) -> {
selectedFileId = CC_FILE_ID selectedFileId = CC_FILE_ID
Log.d(TAG, "SELECT CC file OK") Timber.d("SELECT CC file OK")
SW_OK SW_OK
} }
data.contentEquals(NDEF_FILE_ID) -> { data.contentEquals(NDEF_FILE_ID) -> {
selectedFileId = NDEF_FILE_ID selectedFileId = NDEF_FILE_ID
Log.d(TAG, "SELECT NDEF file OK") Timber.d("SELECT NDEF file OK")
SW_OK SW_OK
} }
else -> { else -> {
Log.w(TAG, "SELECT unknown file: ${data.toHex()}") Timber.w("SELECT unknown file: ${data.toHex()}")
SW_FILE_NOT_FOUND SW_FILE_NOT_FOUND
} }
} }
@@ -127,7 +127,7 @@ class NdefHceService : HostApduService() {
CC_FILE CC_FILE
selectedFileId != null && selectedFileId!!.contentEquals(NDEF_FILE_ID) -> { selectedFileId != null && selectedFileId!!.contentEquals(NDEF_FILE_ID) -> {
ndefFileBytes ?: run { ndefFileBytes ?: run {
Log.w(TAG, "READ BINARY: no NDEF data loaded") Timber.w("READ BINARY: no NDEF data loaded")
return SW_FILE_NOT_FOUND return SW_FILE_NOT_FOUND
} }
} }
@@ -141,7 +141,7 @@ class NdefHceService : HostApduService() {
} }
override fun onDeactivated(reason: Int) { override fun onDeactivated(reason: Int) {
Log.d(TAG, "HCE deactivated, reason=$reason") Timber.d("HCE deactivated, reason=$reason")
selectedFileId = null selectedFileId = null
} }
@@ -8,7 +8,7 @@ import android.nfc.Tag
import android.nfc.tech.IsoDep import android.nfc.tech.IsoDep
import android.nfc.tech.Ndef import android.nfc.tech.Ndef
import android.os.Build import android.os.Build
import android.util.Log import timber.log.Timber
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.ui.NfcOfferState import com.bitcointxoko.gudariwallet.ui.NfcOfferState
@@ -33,7 +33,7 @@ class NfcViewModel : ViewModel() {
* Builds the NDEF file bytes and loads them into NdefHceService. * Builds the NDEF file bytes and loads them into NdefHceService.
*/ */
fun startEmulating(uri: String) { fun startEmulating(uri: String) {
Log.d(TAG, "startEmulating: $uri") Timber.d("startEmulating: $uri")
NdefHceService.ndefFileBytes = buildNdefFileBytes(uri) NdefHceService.ndefFileBytes = buildNdefFileBytes(uri)
_isNfcEmulating.value = true _isNfcEmulating.value = true
} }
@@ -42,7 +42,7 @@ class NfcViewModel : ViewModel() {
* Stop emulating clears the NDEF payload from the HCE service. * Stop emulating clears the NDEF payload from the HCE service.
*/ */
fun stopEmulating() { fun stopEmulating() {
Log.d(TAG, "stopEmulating") Timber.d("stopEmulating")
NdefHceService.ndefFileBytes = null NdefHceService.ndefFileBytes = null
_isNfcEmulating.value = false _isNfcEmulating.value = false
} }
@@ -63,7 +63,7 @@ class NfcViewModel : ViewModel() {
* recognisable payment URI. * recognisable payment URI.
*/ */
fun onTagDiscovered(tag: Tag) { fun onTagDiscovered(tag: Tag) {
Log.d(TAG, "onTagDiscovered: ${tag.id.joinToString("") { "%02X".format(it) }}") Timber.d("onTagDiscovered: ${tag.id.joinToString("") { "%02X".format(it) }}")
viewModelScope.launch(Dispatchers.IO) { // ← move to IO thread viewModelScope.launch(Dispatchers.IO) { // ← move to IO thread
readNdefText(tag) readNdefText(tag)
} }
@@ -87,7 +87,7 @@ class NfcViewModel : ViewModel() {
val message = rawMessages[0] as NdefMessage val message = rawMessages[0] as NdefMessage
val uri = extractUri(message) val uri = extractUri(message)
if (uri != null) { if (uri != null) {
Log.d(TAG, "onNfcIntent (cold-start): uri=$uri") Timber.d("onNfcIntent (cold-start): uri=$uri")
_nfcOfferState.value = NfcOfferState.Detected(uri, SendInputDetector.detect(uri)) _nfcOfferState.value = NfcOfferState.Detected(uri, SendInputDetector.detect(uri))
return return
} }
@@ -127,14 +127,14 @@ class NfcViewModel : ViewModel() {
extractUri(msg) extractUri(msg)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "readNdefText error: ${e.message}") Timber.e("readNdefText error: ${e.message}")
null null
} }
if (uri != null) { if (uri != null) {
Log.d(TAG, "readNdefText: uri=$uri") Timber.d("readNdefText: uri=$uri")
_nfcOfferState.value = NfcOfferState.Detected(uri, SendInputDetector.detect(uri)) _nfcOfferState.value = NfcOfferState.Detected(uri, SendInputDetector.detect(uri))
} else { } else {
Log.w(TAG, "readNdefText: no URI found in tag") Timber.w("readNdefText: no URI found in tag")
} }
} }
/** /**
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.receive package com.bitcointxoko.gudariwallet.ui.receive
import android.util.Log import timber.log.Timber
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.data.WalletRepository import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -32,7 +32,7 @@ class ReceiveViewModel(
fun onIncomingPayment(event: IncomingPaymentEvent) { fun onIncomingPayment(event: IncomingPaymentEvent) {
val rs = _receiveState.value val rs = _receiveState.value
if (rs is ReceiveState.InvoiceReady) { if (rs is ReceiveState.InvoiceReady) {
Log.d(TAG, "BALANCE [WS RECEIVE] Marking invoice ${rs.paymentHash.take(8)}… as received") Timber.d("BALANCE [WS RECEIVE] Marking invoice ${rs.paymentHash.take(8)}… as received")
_receiveState.value = ReceiveState.PaymentReceived( _receiveState.value = ReceiveState.PaymentReceived(
amountSats = event.amountSats, amountSats = event.amountSats,
memo = event.memo, memo = event.memo,
@@ -72,7 +72,7 @@ class ReceiveViewModel(
) )
} }
.onFailure { e -> .onFailure { e ->
Log.e(TAG, "RECEIVE [CREATE INVOICE ERROR] ${e.message}") Timber.e("RECEIVE [CREATE INVOICE ERROR] ${e.message}")
_receiveState.value = ReceiveState.Error( _receiveState.value = ReceiveState.Error(
parseApiError(e, failedToCreateInvoice) // ← localised parseApiError(e, failedToCreateInvoice) // ← localised
) )
@@ -100,7 +100,7 @@ class ReceiveViewModel(
lnurl = lnurl lnurl = lnurl
) )
_navigateToReceive.tryEmit(Unit) _navigateToReceive.tryEmit(Unit)
Log.d(TAG, "LNURL-WITHDRAW [NAV EVENT] min=$minSats max=$maxSats sats — navigating to Receive") Timber.d("LNURL-WITHDRAW [NAV EVENT] min=$minSats max=$maxSats sats — navigating to Receive")
} }
/** /**
@@ -124,19 +124,19 @@ class ReceiveViewModel(
if (invoiceResult.isFailure) { if (invoiceResult.isFailure) {
val e = invoiceResult.exceptionOrNull() val e = invoiceResult.exceptionOrNull()
?: Exception(failedToCreateInvoice) ?: Exception(failedToCreateInvoice)
Log.e(TAG, "LNURL-WITHDRAW [INVOICE ERROR] ${e.message}") Timber.e("LNURL-WITHDRAW [INVOICE ERROR] ${e.message}")
_receiveState.value = ReceiveState.Error( _receiveState.value = ReceiveState.Error(
parseApiError(e, failedToCreateInvoice) // ← localised parseApiError(e, failedToCreateInvoice) // ← localised
) )
return@launch return@launch
} }
val invoice = invoiceResult.getOrThrow() val invoice = invoiceResult.getOrThrow()
Log.d(TAG, "LNURL-WITHDRAW [INVOICE] ${invoice.paymentHash} — submitting to callback") Timber.d("LNURL-WITHDRAW [INVOICE] ${invoice.paymentHash} — submitting to callback")
runCatching { repo.executeWithdraw(callback, k1, invoice.bolt11) } runCatching { repo.executeWithdraw(callback, k1, invoice.bolt11) }
.onSuccess { response -> .onSuccess { response ->
if (response.status == "OK") { if (response.status == "OK") {
Log.d(TAG, "LNURL-WITHDRAW [CALLBACK OK] waiting for push payment") Timber.d("LNURL-WITHDRAW [CALLBACK OK] waiting for push payment")
_receiveState.value = ReceiveState.InvoiceReady( _receiveState.value = ReceiveState.InvoiceReady(
bolt11 = invoice.bolt11, bolt11 = invoice.bolt11,
paymentHash = invoice.paymentHash, paymentHash = invoice.paymentHash,
@@ -147,12 +147,12 @@ class ReceiveViewModel(
} else { } else {
val reason = response.reason?.ifBlank { null } val reason = response.reason?.ifBlank { null }
?: withdrawRejected // ← localised ?: withdrawRejected // ← localised
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK ERR] $reason") Timber.w("LNURL-WITHDRAW [CALLBACK ERR] $reason")
_receiveState.value = ReceiveState.Error(reason) _receiveState.value = ReceiveState.Error(reason)
} }
} }
.onFailure { e -> .onFailure { e ->
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}") Timber.w("LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}")
_receiveState.value = ReceiveState.Error( _receiveState.value = ReceiveState.Error(
parseApiError(e, withdrawFailed) // ← localised parseApiError(e, withdrawFailed) // ← localised
) )
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -28,12 +28,12 @@ class LnurlPayer(
// ── 2. Terminal error — don't retry // ── 2. Terminal error — don't retry
val initialError = initial.exceptionOrNull() ?: Exception("Payment failed") val initialError = initial.exceptionOrNull() ?: Exception("Payment failed")
if (isTerminalPayError(initialError)) { if (isTerminalPayError(initialError)) {
Log.w(TAG, "LNURL [PAY TERMINAL] ${initialError.message}") Timber.w("LNURL [PAY TERMINAL] ${initialError.message}")
return Result.failure(initialError) return Result.failure(initialError)
} }
// ── 3. Invalidate cache and rescan // ── 3. Invalidate cache and rescan
Log.w(TAG, "LNURL [PAY RETRY] invalidating cache and rescanning") Timber.w("LNURL [PAY RETRY] invalidating cache and rescanning")
cache.invalidate(lnurl) cache.invalidate(lnurl)
val rescanResult = scanner.rescan(lnurl) val rescanResult = scanner.rescan(lnurl)
@@ -44,7 +44,7 @@ class LnurlPayer(
val protocolError = lnurlProtocolError(freshRaw) val protocolError = lnurlProtocolError(freshRaw)
if (protocolError != null) { if (protocolError != null) {
Log.w(TAG, "LNURL [RESCAN PROTO] $protocolError") Timber.w("LNURL [RESCAN PROTO] $protocolError")
return Result.failure(Exception(protocolError)) return Result.failure(Exception(protocolError))
} }
cache.put(lnurl, freshRaw) cache.put(lnurl, freshRaw)
@@ -53,7 +53,7 @@ class LnurlPayer(
val retry = tryWithFallback(freshRaw, lnurl, amountMsat, comment, "RETRY") val retry = tryWithFallback(freshRaw, lnurl, amountMsat, comment, "RETRY")
if (retry.isSuccess) return retry if (retry.isSuccess) return retry
Log.e(TAG, "LNURL [PAY FINAL ERR] ${retry.exceptionOrNull()?.message}") Timber.e("LNURL [PAY FINAL ERR] ${retry.exceptionOrNull()?.message}")
return Result.failure(retry.exceptionOrNull() ?: Exception("Payment failed")) return Result.failure(retry.exceptionOrNull() ?: Exception("Payment failed"))
} }
@@ -66,17 +66,17 @@ class LnurlPayer(
): Result<String> { ): Result<String> {
val direct = runCatching { repo.payLnurlDirect(rawRes, amountMsat, comment) } val direct = runCatching { repo.payLnurlDirect(rawRes, amountMsat, comment) }
if (direct.isSuccess) { if (direct.isSuccess) {
Log.d(TAG, "LNURL [PAY $label DIRECT] success") Timber.d("LNURL [PAY $label DIRECT] success")
return Result.success(direct.getOrThrow().paymentHash) return Result.success(direct.getOrThrow().paymentHash)
} }
Log.w(TAG, "LNURL [PAY $label DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy") Timber.w("LNURL [PAY $label DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy")
val proxy = runCatching { repo.payLnurl(rawRes, lnurl, amountMsat, comment) } val proxy = runCatching { repo.payLnurl(rawRes, lnurl, amountMsat, comment) }
if (proxy.isSuccess) { if (proxy.isSuccess) {
Log.d(TAG, "LNURL [PAY $label PROXY] success") Timber.d("LNURL [PAY $label PROXY] success")
return Result.success(proxy.getOrThrow().paymentHash) return Result.success(proxy.getOrThrow().paymentHash)
} }
Log.w(TAG, "LNURL [PAY $label PROXY FAIL] ${proxy.exceptionOrNull()?.message}") Timber.w("LNURL [PAY $label PROXY FAIL] ${proxy.exceptionOrNull()?.message}")
return Result.failure(proxy.exceptionOrNull() ?: Exception("Payment failed")) return Result.failure(proxy.exceptionOrNull() ?: Exception("Payment failed"))
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -18,25 +18,25 @@ class LnurlScanner(
// 1. Cache hit // 1. Cache hit
val cached = lnurlCache.get(lnurl) val cached = lnurlCache.get(lnurl)
if (cached != null) { if (cached != null) {
Log.d(TAG, "LNURL [SCAN CACHE ] $lnurl") Timber.d("LNURL [SCAN CACHE ] $lnurl")
return Result.success(LnurlScanResult(cached, fromCache = true)) return Result.success(LnurlScanResult(cached, fromCache = true))
} }
// 2. Direct // 2. Direct
val direct = runCatching { repo.scanLnurlDirect(lnurl) } val direct = runCatching { repo.scanLnurlDirect(lnurl) }
if (direct.isSuccess) { if (direct.isSuccess) {
Log.d(TAG, "LNURL [SCAN DIRECT] tag=${direct.getOrThrow().tag}") Timber.d("LNURL [SCAN DIRECT] tag=${direct.getOrThrow().tag}")
return Result.success(LnurlScanResult(direct.getOrThrow().rawRes, fromCache = false)) return Result.success(LnurlScanResult(direct.getOrThrow().rawRes, fromCache = false))
} }
Log.w(TAG, "LNURL [SCAN DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy") Timber.w("LNURL [SCAN DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy")
// 3. Proxy fallback // 3. Proxy fallback
val proxy = runCatching { repo.scanLnurl(lnurl) } val proxy = runCatching { repo.scanLnurl(lnurl) }
if (proxy.isSuccess) { if (proxy.isSuccess) {
Log.d(TAG, "LNURL [SCAN PROXY ] tag=${proxy.getOrThrow().tag}") Timber.d("LNURL [SCAN PROXY ] tag=${proxy.getOrThrow().tag}")
return Result.success(LnurlScanResult(proxy.getOrThrow().rawRes, fromCache = false)) return Result.success(LnurlScanResult(proxy.getOrThrow().rawRes, fromCache = false))
} }
Log.e(TAG, "LNURL [SCAN PROXY FAIL] ${proxy.exceptionOrNull()?.message}") Timber.e("LNURL [SCAN PROXY FAIL] ${proxy.exceptionOrNull()?.message}")
return Result.failure(proxy.exceptionOrNull() ?: Exception("Scan failed")) return Result.failure(proxy.exceptionOrNull() ?: Exception("Scan failed"))
} }
@@ -44,15 +44,15 @@ class LnurlScanner(
suspend fun rescan(lnurl: String): Result<LnurlScanResponse> { suspend fun rescan(lnurl: String): Result<LnurlScanResponse> {
val direct = runCatching { repo.scanLnurlDirect(lnurl) } val direct = runCatching { repo.scanLnurlDirect(lnurl) }
if (direct.isSuccess) { if (direct.isSuccess) {
Log.d(TAG, "LNURL [RESCAN DIRECT] success") Timber.d("LNURL [RESCAN DIRECT] success")
return Result.success(direct.getOrThrow().rawRes) return Result.success(direct.getOrThrow().rawRes)
} }
Log.w(TAG, "LNURL [RESCAN DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy") Timber.w("LNURL [RESCAN DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy")
val proxy = runCatching { repo.scanLnurl(lnurl) } val proxy = runCatching { repo.scanLnurl(lnurl) }
if (proxy.isSuccess) return Result.success(proxy.getOrThrow().rawRes) if (proxy.isSuccess) return Result.success(proxy.getOrThrow().rawRes)
Log.e(TAG, "LNURL [RESCAN ERR] ${proxy.exceptionOrNull()?.message}") Timber.e("LNURL [RESCAN ERR] ${proxy.exceptionOrNull()?.message}")
return Result.failure(proxy.exceptionOrNull() ?: Exception("Rescan failed")) return Result.failure(proxy.exceptionOrNull() ?: Exception("Rescan failed"))
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.data.WalletRepository import com.bitcointxoko.gudariwallet.data.WalletRepository
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -25,21 +25,21 @@ class PaymentPoller(private val repo: WalletRepository) {
when { when {
detail?.paid == true -> { detail?.paid == true -> {
Log.d(TAG, "HOLD INVOICE [POLL $attempt] settled — $paymentHash") Timber.d("HOLD INVOICE [POLL $attempt] settled — $paymentHash")
return PollResult.Settled(detail.details?.feeSat) return PollResult.Settled(detail.details?.feeSat)
} }
detail?.details?.status == "pending" || detail?.details?.pending == true -> { detail?.details?.status == "pending" || detail?.details?.pending == true -> {
Log.d(TAG, "HOLD INVOICE [POLL $attempt] still pending — $paymentHash") Timber.d("HOLD INVOICE [POLL $attempt] still pending — $paymentHash")
// continue // continue
} }
else -> { else -> {
Log.w(TAG, "HOLD INVOICE [POLL $attempt] payment failed — $paymentHash") Timber.w("HOLD INVOICE [POLL $attempt] payment failed — $paymentHash")
return PollResult.Failed return PollResult.Failed
} }
} }
} }
Log.w(TAG, "HOLD INVOICE [POLL TIMEOUT] $paymentHash") Timber.w("HOLD INVOICE [POLL TIMEOUT] $paymentHash")
return PollResult.TimedOut return PollResult.TimedOut
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log import timber.log.Timber
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
@@ -78,7 +78,7 @@ class SendViewModel(
fun scan(rawInput: String, strings: SendStrings) { fun scan(rawInput: String, strings: SendStrings) {
val normalized = SendInputDetector.normalize(rawInput) val normalized = SendInputDetector.normalize(rawInput)
val type = SendInputDetector.detect(rawInput) val type = SendInputDetector.detect(rawInput)
Log.d(TAG, "SCAN [DETECT] input='$rawInput' → type=$type") Timber.d("SCAN [DETECT] input='$rawInput' → type=$type")
when (type) { when (type) {
SendInputType.Unknown -> { SendInputType.Unknown -> {
@@ -100,11 +100,11 @@ class SendViewModel(
viewModelScope.launch { viewModelScope.launch {
val decoded = _sendState.value as? SendState.Bolt11Decoded val decoded = _sendState.value as? SendState.Bolt11Decoded
if (decoded == null) { if (decoded == null) {
Log.w(TAG, "SEND [PAY BOLT11] called in unexpected state: ${_sendState.value::class.simpleName}") Timber.w("SEND [PAY BOLT11] called in unexpected state: ${_sendState.value::class.simpleName}")
return@launch return@launch
} }
_sendState.value = SendState.Paying _sendState.value = SendState.Paying
Log.d(TAG, "SEND [PAY BOLT11 START] Sending invoice to server") Timber.d("SEND [PAY BOLT11 START] Sending invoice to server")
runCatching { repo.payBolt11(bolt11) } runCatching { repo.payBolt11(bolt11) }
.onSuccess { response -> .onSuccess { response ->
handlePaymentResult( handlePaymentResult(
@@ -118,7 +118,7 @@ class SendViewModel(
) )
} }
.onFailure { e -> .onFailure { e ->
Log.e(TAG, "SEND [PAY BOLT11 ERROR] ${e.message}") Timber.e("SEND [PAY BOLT11 ERROR] ${e.message}")
_sendState.value = SendState.Error( _sendState.value = SendState.Error(
parseApiError(e, strings.paymentFailed) // ← "Payment failed" parseApiError(e, strings.paymentFailed) // ← "Payment failed"
) )
@@ -238,7 +238,7 @@ class SendViewModel(
val bolt11 = runCatching { val bolt11 = runCatching {
repo.fetchLnurlCallbackInvoice(rawRes, amountMsat, comment) repo.fetchLnurlCallbackInvoice(rawRes, amountMsat, comment)
}.getOrElse { e -> }.getOrElse { e ->
Log.e(TAG, "LNURL [FETCH INVOICE ERR] ${e.message}") Timber.e("LNURL [FETCH INVOICE ERR] ${e.message}")
_sendState.value = SendState.Error( _sendState.value = SendState.Error(
parseApiError(e, strings.couldNotFetchInvoice) // ← "Could not fetch invoice from recipient" parseApiError(e, strings.couldNotFetchInvoice) // ← "Could not fetch invoice from recipient"
) )
@@ -248,7 +248,7 @@ class SendViewModel(
val decoded = runCatching { val decoded = runCatching {
repo.decodeBolt11(bolt11) repo.decodeBolt11(bolt11)
}.getOrElse { e -> }.getOrElse { e ->
Log.e(TAG, "LNURL [DECODE ERR] ${e.message}") Timber.e("LNURL [DECODE ERR] ${e.message}")
_sendState.value = SendState.Error( _sendState.value = SendState.Error(
parseApiError(e, strings.couldNotDecodeInvoice) // ← "Could not decode invoice" parseApiError(e, strings.couldNotDecodeInvoice) // ← "Could not decode invoice"
) )
@@ -260,7 +260,7 @@ class SendViewModel(
hints = decoded.routeHints hints = decoded.routeHints
) )
Log.d(TAG, "LNURL [INVOICE READY] amountMsat=$amountMsat " + Timber.d("LNURL [INVOICE READY] amountMsat=$amountMsat " +
"routeRisk=${routeRisk?.level} hints=${decoded.routeHints.size}") "routeRisk=${routeRisk?.level} hints=${decoded.routeHints.size}")
_sendState.value = SendState.LnurlInvoiceReady( _sendState.value = SendState.LnurlInvoiceReady(
@@ -315,7 +315,7 @@ class SendViewModel(
) )
return@launch return@launch
} }
Log.w(TAG, "LNURL [PAY INVOICE DIRECT FAIL] " + Timber.w("LNURL [PAY INVOICE DIRECT FAIL] " +
"${directResult.exceptionOrNull()?.message} — falling back to payLnurl") "${directResult.exceptionOrNull()?.message} — falling back to payLnurl")
executeLnurlPayment(state.rawRes, state.lnurl, state.amountMsat, state.comment, strings) executeLnurlPayment(state.rawRes, state.lnurl, state.amountMsat, state.comment, strings)
} }
@@ -347,12 +347,12 @@ class SendViewModel(
return return
} }
val httpsUrl = "https://" + rawInput.trim().substringAfter("://") val httpsUrl = "https://" + rawInput.trim().substringAfter("://")
Log.d(TAG, "LUD17 [FETCH] $httpsUrl") Timber.d("LUD17 [FETCH] $httpsUrl")
_sendState.value = SendState.Scanning _sendState.value = SendState.Scanning
viewModelScope.launch { viewModelScope.launch {
runCatching { repo.fetchLnurlDirect(httpsUrl) } runCatching { repo.fetchLnurlDirect(httpsUrl) }
.onSuccess { raw -> .onSuccess { raw ->
Log.d(TAG, "LUD17 [RESPONSE] tag=${raw.tag}") Timber.d("LUD17 [RESPONSE] tag=${raw.tag}")
when (raw.tag) { when (raw.tag) {
"withdrawRequest" -> {emitWithdrawScanned(raw, httpsUrl)} "withdrawRequest" -> {emitWithdrawScanned(raw, httpsUrl)}
"payRequest" -> { "payRequest" -> {
@@ -365,7 +365,7 @@ class SendViewModel(
} }
} }
.onFailure { e -> .onFailure { e ->
Log.e(TAG, "LUD17 [ERROR] ${e.message}") Timber.e("LUD17 [ERROR] ${e.message}")
_sendState.value = SendState.Error( _sendState.value = SendState.Error(
parseApiError(e, strings.couldNotResolveAddress) // ← "Could not resolve address" parseApiError(e, strings.couldNotResolveAddress) // ← "Could not resolve address"
) )
@@ -410,7 +410,7 @@ class SendViewModel(
) )
} }
.onFailure { e -> .onFailure { e ->
Log.e(TAG, "SEND [DECODE ERROR] ${e.message}") Timber.e("SEND [DECODE ERROR] ${e.message}")
_sendState.value = SendState.Error( _sendState.value = SendState.Error(
parseApiError(e, strings.couldNotDecodeInvoice) // ← "Could not decode invoice" parseApiError(e, strings.couldNotDecodeInvoice) // ← "Could not decode invoice"
) )
@@ -428,7 +428,7 @@ class SendViewModel(
val protocolError = lnurlProtocolError(raw) val protocolError = lnurlProtocolError(raw)
if (protocolError != null) { if (protocolError != null) {
Log.w(TAG, "LNURL [SCAN PROTO ERR] $protocolError") Timber.w("LNURL [SCAN PROTO ERR] $protocolError")
_sendState.value = SendState.Error(protocolError) _sendState.value = SendState.Error(protocolError)
return@launch return@launch
} }
@@ -2,7 +2,7 @@ package com.bitcointxoko.gudariwallet.util
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Log import timber.log.Timber
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -61,7 +61,7 @@ object BiometricHelper {
if (code != BiometricPrompt.ERROR_USER_CANCELED && if (code != BiometricPrompt.ERROR_USER_CANCELED &&
code != BiometricPrompt.ERROR_NEGATIVE_BUTTON code != BiometricPrompt.ERROR_NEGATIVE_BUTTON
) { ) {
Log.e(TAG, "Biometric error $code: $msg") Timber.e("Biometric error $code: $msg")
onError(code, msg) onError(code, msg)
} }
} }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.util package com.bitcointxoko.gudariwallet.util
import android.util.Log import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.RouteHintHop import com.bitcointxoko.gudariwallet.api.RouteHintHop
import com.bitcointxoko.gudariwallet.data.DecodedBolt11 import com.bitcointxoko.gudariwallet.data.DecodedBolt11
import fr.acinq.secp256k1.Secp256k1 import fr.acinq.secp256k1.Secp256k1
@@ -59,7 +59,7 @@ object Bolt11Decoder {
// ── 1. Split HRP / data ─────────────────────────────────────────────── // ── 1. Split HRP / data ───────────────────────────────────────────────
val sepIdx = lower.lastIndexOf('1') val sepIdx = lower.lastIndexOf('1')
if (sepIdx < 1) { if (sepIdx < 1) {
Log.w(TAG, "No bech32 separator found") Timber.w("No bech32 separator found")
return null return null
} }
@@ -67,21 +67,21 @@ object Bolt11Decoder {
val dataChars = lower.substring(sepIdx + 1) val dataChars = lower.substring(sepIdx + 1)
if (dataChars.length < 6 + SIGNATURE_GROUPS + 7) { if (dataChars.length < 6 + SIGNATURE_GROUPS + 7) {
Log.w(TAG, "Data part too short: ${dataChars.length} chars") Timber.w("Data part too short: ${dataChars.length} chars")
return null return null
} }
// ── 2. Parse HRP ────────────────────────────────────────────────────── // ── 2. Parse HRP ──────────────────────────────────────────────────────
val prefix = KNOWN_PREFIXES.firstOrNull { hrp.startsWith(it) } val prefix = KNOWN_PREFIXES.firstOrNull { hrp.startsWith(it) }
?: run { ?: run {
Log.w(TAG, "Unknown HRP prefix: $hrp") Timber.w("Unknown HRP prefix: $hrp")
return null return null
} }
val amountStr = hrp.removePrefix(prefix) val amountStr = hrp.removePrefix(prefix)
val amountMsat = parseAmountMsat(amountStr) val amountMsat = parseAmountMsat(amountStr)
?: run { ?: run {
Log.w(TAG, "Could not parse amount from HRP: '$amountStr'") Timber.w("Could not parse amount from HRP: '$amountStr'")
return null return null
} }
@@ -98,7 +98,7 @@ object Bolt11Decoder {
val groups = payload.map { c -> val groups = payload.map { c ->
val idx = CHARSET.indexOf(c) val idx = CHARSET.indexOf(c)
if (idx < 0) { if (idx < 0) {
Log.w(TAG, "Invalid bech32 character: '$c'") Timber.w("Invalid bech32 character: '$c'")
return null return null
} }
idx idx
@@ -108,7 +108,7 @@ object Bolt11Decoder {
val sigGroups = groups.takeLast(SIGNATURE_GROUPS) val sigGroups = groups.takeLast(SIGNATURE_GROUPS)
val signedGroups = groups.dropLast(SIGNATURE_GROUPS) val signedGroups = groups.dropLast(SIGNATURE_GROUPS)
if (signedGroups.size < 7) { if (signedGroups.size < 7) {
Log.w(TAG, "Data part too short for timestamp") Timber.w("Data part too short for timestamp")
return null return null
} }
@@ -139,7 +139,7 @@ object Bolt11Decoder {
pos += 3 pos += 3
if (pos + dataLen > taggedGroups.size) { if (pos + dataLen > taggedGroups.size) {
Log.w(TAG, "Tagged field overflows data (type=$type, len=$dataLen)") Timber.w("Tagged field overflows data (type=$type, len=$dataLen)")
break break
} }
@@ -152,7 +152,7 @@ object Bolt11Decoder {
TAG_PAYMENT_HASH -> { TAG_PAYMENT_HASH -> {
// Fixed: 52 groups × 5 = 260 bits → 32 bytes + 4 padding bits // Fixed: 52 groups × 5 = 260 bits → 32 bytes + 4 padding bits
if (fieldGroups.size != 52) { if (fieldGroups.size != 52) {
Log.w(TAG, "Malformed 'p' field (len=${fieldGroups.size}, expected 52)") Timber.w("Malformed 'p' field (len=${fieldGroups.size}, expected 52)")
continue continue
} }
val bytes = convertBits(fieldGroups, 5, 8, false) ?: continue val bytes = convertBits(fieldGroups, 5, 8, false) ?: continue
@@ -166,7 +166,7 @@ object Bolt11Decoder {
TAG_PAYEE_PUBKEY -> { TAG_PAYEE_PUBKEY -> {
if (fieldGroups.size != 53) { if (fieldGroups.size != 53) {
Log.w(TAG, "Malformed 'n' field (len=${fieldGroups.size}, expected 53)") Timber.w("Malformed 'n' field (len=${fieldGroups.size}, expected 53)")
continue continue
} }
val bytes = convertBits(fieldGroups, 5, 8, false) ?: continue val bytes = convertBits(fieldGroups, 5, 8, false) ?: continue
@@ -185,7 +185,7 @@ object Bolt11Decoder {
TAG_FALLBACK_ADDR -> { TAG_FALLBACK_ADDR -> {
fallbackAddress = decodeFallbackAddress(fieldGroups, addrHrp) fallbackAddress = decodeFallbackAddress(fieldGroups, addrHrp)
if (fallbackAddress == null) { if (fallbackAddress == null) {
Log.w(TAG, "Could not decode fallback address") Timber.w("Could not decode fallback address")
} }
} }
@@ -217,18 +217,18 @@ object Bolt11Decoder {
// ── 6. Recover payee from signature if 'n' field was absent ─────────── // ── 6. Recover payee from signature if 'n' field was absent ───────────
if (payee == null) { if (payee == null) {
payee = recoverPayee(hrp, signedGroups, sigGroups) payee = recoverPayee(hrp, signedGroups, sigGroups)
if (payee != null) Log.d(TAG, "Recovered payee pubkey from signature") if (payee != null) Timber.d("Recovered payee pubkey from signature")
} }
// ── NEW: paymentHash is required — fail loudly if absent ───────────── // ── NEW: paymentHash is required — fail loudly if absent ─────────────
if (paymentHash == null) { if (paymentHash == null) {
Log.w(TAG, "Invoice missing required 'p' (payment hash) field") Timber.w("Invoice missing required 'p' (payment hash) field")
return null return null
} }
val routeHints = routeHintRoutes.flatten() val routeHints = routeHintRoutes.flatten()
Log.d(TAG, "Decoded locally — amount: ${amountSats}sat, memo: \"$memo\", " + Timber.d("Decoded locally — amount: ${amountSats}sat, memo: \"$memo\", " +
"payee: $payee, routes: ${routeHintRoutes.size}, hops: ${routeHints.size}, " + "payee: $payee, routes: ${routeHintRoutes.size}, hops: ${routeHints.size}, " +
"createdAt: $createdAt, expiresAt: $expiresAt, " + "createdAt: $createdAt, expiresAt: $expiresAt, " +
"fallback: $fallbackAddress, paymentHash: $paymentHash") "fallback: $fallbackAddress, paymentHash: $paymentHash")
@@ -287,7 +287,7 @@ object Bolt11Decoder {
encodeBech32Address(addrHrp, witnessVersion = 0, dataGroups) encodeBech32Address(addrHrp, witnessVersion = 0, dataGroups)
} }
else -> { else -> {
Log.w(TAG, "Unknown fallback address version: $version") Timber.w("Unknown fallback address version: $version")
null null
} }
} }
@@ -372,20 +372,20 @@ object Bolt11Decoder {
val sigBytes = convertBits(sigGroups, 5, 8, false)?.toByteArray() ?: return null val sigBytes = convertBits(sigGroups, 5, 8, false)?.toByteArray() ?: return null
if (sigBytes.size != 65) { if (sigBytes.size != 65) {
Log.w(TAG, "Unexpected signature length: ${sigBytes.size}") Timber.w("Unexpected signature length: ${sigBytes.size}")
return null return null
} }
val compactSig = sigBytes.copyOfRange(0, 64) val compactSig = sigBytes.copyOfRange(0, 64)
val recid = sigBytes[64].toInt() val recid = sigBytes[64].toInt()
if (recid !in 0..3) { if (recid !in 0..3) {
Log.w(TAG, "Invalid recovery id: $recid") Timber.w("Invalid recovery id: $recid")
return null return null
} }
val uncompressed = Secp256k1.ecdsaRecover(compactSig, message, recid) val uncompressed = Secp256k1.ecdsaRecover(compactSig, message, recid)
Secp256k1.pubKeyCompress(uncompressed).toHex() Secp256k1.pubKeyCompress(uncompressed).toHex()
}.getOrElse { }.getOrElse {
Log.w(TAG, "Payee recovery failed: ${it.message}") Timber.w("Payee recovery failed: ${it.message}")
null null
} }
+2
View File
@@ -33,6 +33,7 @@ work = "2.9.1"
benchmark = "1.4.1" benchmark = "1.4.1"
runner = "1.5.2" runner = "1.5.2"
androidx-test-ext = "1.2.1" androidx-test-ext = "1.2.1"
timber = "5.0.1"
[libraries] [libraries]
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
@@ -81,6 +82,7 @@ androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx"
androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmark" } androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmark" }
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext" }
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }