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
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionName = "0.1.0"
// testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
@@ -160,11 +160,14 @@ dependencies {
// secp256k1
implementation(libs.secp256k1.kmp.android)
// lyricist
implementation(libs.lyricist)
// Required for @LyricistStrings code generation
ksp(libs.lyricist.processor)
// timber
implementation(libs.timber)
}
protobuf {
@@ -9,6 +9,7 @@ import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
import com.bitcointxoko.gudariwallet.sync.HistoricalSyncWorkerFactory
import com.bitcointxoko.gudariwallet.util.NotificationHelper
import timber.log.Timber
class GudariWalletApplication : Application(), Configuration.Provider {
@@ -29,5 +30,8 @@ class GudariWalletApplication : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
NotificationHelper.createChannels(this) // moved from MainActivity
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
}
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data
import android.content.Context
import android.util.Log
import timber.log.Timber
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
@@ -47,7 +47,7 @@ class BalancePrefsStore(context: Context) {
val prefs = dataStore.data
.catch { e ->
if (e is IOException) {
Log.w(TAG, "BALANCE [SATS ] read error — ${e.message}")
Timber.w("BALANCE [SATS ] read error — ${e.message}")
emit(emptyPreferences())
} else throw e
}
@@ -62,7 +62,7 @@ class BalancePrefsStore(context: Context) {
val prefs = dataStore.data
.catch { e ->
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())
} else throw e
}
@@ -80,9 +80,9 @@ class BalancePrefsStore(context: Context) {
prefs[BALANCE_SAVED_AT] = System.currentTimeMillis()
}
}.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 ────────────────────────────────────────────────────
@@ -94,7 +94,7 @@ class BalancePrefsStore(context: Context) {
val isBalanceHiddenFlow: Flow<Boolean> = dataStore.data
.catch { e ->
if (e is IOException) {
Log.w(TAG, "BALANCE [HIDDEN ] read error — ${e.message}")
Timber.w("BALANCE [HIDDEN ] read error — ${e.message}")
emit(emptyPreferences())
} else throw e
}
@@ -104,7 +104,7 @@ class BalancePrefsStore(context: Context) {
runCatching {
dataStore.edit { prefs -> prefs[BALANCE_HIDDEN] = hidden }
}.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
.catch { e ->
if (e is IOException) {
Log.w(TAG, "BALANCE [CURRENCY] read error — ${e.message}")
Timber.w("BALANCE [CURRENCY] read error — ${e.message}")
emit(emptyPreferences())
} else throw e
}
@@ -129,7 +129,7 @@ class BalancePrefsStore(context: Context) {
else prefs.remove(FIAT_CURRENCY)
}
}.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
import android.content.Context
import android.util.Log
import timber.log.Timber
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
@@ -45,14 +45,14 @@ class FiatDataStore(context: Context) {
val prefs = dataStore.data
.catch { e ->
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())
} else throw e
}
.first()
val value = prefs[rateKey(currency)]?.toDouble()
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 }
}
@@ -68,9 +68,9 @@ class FiatDataStore(context: Context) {
prefs[rateFetchedAtKey(currency)] = System.currentTimeMillis()
}
}.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 ─────────────────────────────────────────────────────────
@@ -82,7 +82,7 @@ class FiatDataStore(context: Context) {
val prefs = dataStore.data
.catch { e ->
if (e is IOException) {
Log.w(TAG, "FIAT [CURRENCIES] read error — ${e.message}")
Timber.w("FIAT [CURRENCIES] read error — ${e.message}")
emit(emptyPreferences())
} else throw e
}
@@ -91,10 +91,10 @@ class FiatDataStore(context: Context) {
return runCatching {
val type = object : TypeToken<List<String>>() {}.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
}.getOrElse { e ->
Log.w(TAG, "FIAT [CURRENCIES] parse failed — ${e.message}")
Timber.w("FIAT [CURRENCIES] parse failed — ${e.message}")
null
}
}
@@ -109,8 +109,8 @@ class FiatDataStore(context: Context) {
prefs[currenciesKey] = gson.toJson(currencies)
}
}.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
import android.util.Log
import timber.log.Timber
private const val TAG = "FiatRateCache"
@@ -29,14 +29,14 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
val age = now - rateLastFetchedAt
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
}
// Cold-start fallback: last persisted rate from DataStore
val persisted = fiatDataStore.getPersistedRate(currency)
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
}
@@ -52,7 +52,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
rateLastFetchedAt = now
cachedRateCurrency = currency
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). */
@@ -72,7 +72,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
*/
suspend fun getCurrencies(): List<String>? {
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
}
@@ -88,7 +88,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
if (currencies.isEmpty()) return
cachedCurrencies = currencies
fiatDataStore.persistCurrencies(currencies)
Log.d(TAG, "FIAT [CURRENCIES] cached ${currencies.size} currencies")
Timber.d("FIAT [CURRENCIES] cached ${currencies.size} currencies")
}
val currentRate: Double?
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data
import android.net.Uri
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.*
import com.bitcointxoko.gudariwallet.security.SecretStore
import com.bitcointxoko.gudariwallet.util.Bolt11Decoder
@@ -52,14 +52,14 @@ class LNbitsWalletRepository(
// ── 1. Try local decode first (no network, no latency) ───────────────
val local = runCatching { Bolt11Decoder.decode(bolt11) }.getOrNull()
if (local != null) {
Log.i("WalletRepository", "decodeBolt11: decoded locally ✓")
Timber.i("decodeBolt11: decoded locally ✓")
return local
}
// ── 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))
Log.i("WalletRepository", "decodeBolt11: decoded via network ✓")
Timber.i("decodeBolt11: decoded via network ✓")
val amountSats = response.amountMsat
?.takeIf { it > 0L }
@@ -75,7 +75,7 @@ class LNbitsWalletRepository(
// createdAt / expiresAt / fallbackAddress are not returned by the API —
// use safe defaults and warn so we know if this path is hit in practice.
Log.w("WalletRepository", "decodeBolt11: network response has no timestamp/expiry — " +
Timber.w("decodeBolt11: network response has no timestamp/expiry — " +
"using Instant.now() + 1h as fallback")
val createdAt = Instant.now()
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)
return ScannedLnurl(
@@ -160,7 +160,7 @@ class LNbitsWalletRepository(
comment : String?
): PaymentConfirmation {
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)
}
@@ -186,7 +186,7 @@ class LNbitsWalletRepository(
append(Uri.encode(comment))
}
}
Log.d(TAG, "LNURL [FETCH INVOICE] callback=$callbackUrl")
Timber.d("LNURL [FETCH INVOICE] callback=$callbackUrl")
val callbackResponse = api.fetchLnurlCallback(callbackUrl)
if (callbackResponse.status == "ERROR") {
throw RuntimeException(
@@ -246,7 +246,7 @@ class LNbitsWalletRepository(
append("&pr=")
append(Uri.encode(bolt11))
}
Log.d(TAG, "LNURL-WITHDRAW [CALLBACK] $url")
Timber.d("LNURL-WITHDRAW [CALLBACK] $url")
return api.withdrawCallback(url)
}
@@ -265,12 +265,12 @@ class LNbitsWalletRepository(
// ── Fiat ──────────────────────────────────────────────────────────────────
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
}
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()
}
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data
import android.content.Context
import android.util.Log
import timber.log.Timber
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
@@ -38,7 +38,7 @@ class LightningAddressStore(private val context: Context) {
.map { prefs ->
prefs[KEY_ADDRESS].also { 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)") }
}
@@ -53,7 +53,7 @@ class LightningAddressStore(private val context: Context) {
val fetchedAt = prefs[KEY_FETCHED_AT] ?: 0L
val age = System.currentTimeMillis() - fetchedAt
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 " +
if (isStale) "→ cache stale, will fetch from API"
else "→ cache fresh, skipping API call")
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data
import android.content.Context
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.GsonProvider
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.data.db.AppDatabase
@@ -34,13 +34,13 @@ class LnurlCacheRepository(context: Context) {
val ageMs = System.currentTimeMillis() - entity.savedAt
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)
return null
}
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)
return null
}
@@ -48,11 +48,11 @@ class LnurlCacheRepository(context: Context) {
// LUD-06: disposable == null means treat as true — do not serve from cache
val disposable = response.disposable
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
}
Log.d(TAG, "LNURL [CACHE HIT ] $key (age ${ageMs / 1000}s)")
Timber.d("LNURL [CACHE HIT ] $key (age ${ageMs / 1000}s)")
return response
}
@@ -63,7 +63,7 @@ class LnurlCacheRepository(context: Context) {
suspend fun put(key: String, response: LnurlScanResponse) {
val disposable = response.disposable
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
}
val entity = LnurlCacheEntity(
@@ -74,9 +74,9 @@ class LnurlCacheRepository(context: Context) {
runCatching {
dao.insert(entity)
memCache[key] = entity
Log.d(TAG, "LNURL [CACHE SET ] $key")
Timber.d("LNURL [CACHE SET ] $key")
}.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) {
evict(key)
Log.d(TAG, "LNURL [INVALIDATED] $key")
Timber.d("LNURL [INVALIDATED] $key")
}
suspend fun clearAll() {
memCache.clear()
runCatching { dao.deleteAll() }
.onFailure { Log.w(TAG, "LNURL [CLEAR ERR ] ${it.message}") }
Log.d(TAG, "LNURL [CLEARED ] all entries")
.onFailure { Timber.w("LNURL [CLEAR ERR ] ${it.message}") }
Timber.d("LNURL [CLEARED ] all entries")
}
// ── Helpers ────────────────────────────────────────────────────────────────
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data
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.NodeAliasCacheEntity
import com.bitcointxoko.gudariwallet.util.WalletConstants
@@ -18,15 +18,15 @@ class NodeAliasCacheRepository(context: Context) {
*/
suspend fun get(pubkey: String): String? {
val entry = dao.getByPubkey(pubkey) ?: run {
Log.d(TAG, "ALIAS [CACHE MISS ] $pubkey")
Timber.d("ALIAS [CACHE MISS ] $pubkey")
return null
}
val ageMs = System.currentTimeMillis() - entry.savedAt
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
} 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
}
}
@@ -40,7 +40,7 @@ class NodeAliasCacheRepository(context: Context) {
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()
.filter { now - it.savedAt < WalletConstants.ALIAS_PERSIST_TTL_MS }
.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. */
@@ -63,6 +63,6 @@ class NodeAliasCacheRepository(context: Context) {
/** Wipe the entire alias cache — mirrors [clearPersistedCache]. */
suspend fun clearAll() {
dao.deleteAll()
Log.d(TAG, "ALIAS [PERSIST ] Cache cleared")
Timber.d("ALIAS [PERSIST ] Cache cleared")
}
}
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.data
import android.content.Context
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.PaymentDetailResponse
import com.bitcointxoko.gudariwallet.api.PaymentRecord
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. */
suspend fun loadCachedPayments(): List<PaymentRecord> {
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()
}
val ageMs = System.currentTimeMillis() - savedAt
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 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() }
.also {
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
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()
runCatching {
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 {
Log.w(TAG, "PAYMENTS [MERGE ERR] ${it.message}")
Timber.w("PAYMENTS [MERGE ERR] ${it.message}")
}
}
@@ -71,9 +71,9 @@ class PaymentCacheRepository(context: Context) {
runCatching {
dao.deleteAll()
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 {
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,
maxCreatedAt = filter.maxCreatedAt
).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} " +
"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. */
fun getCachedDetail(checkingId: String): PaymentDetailResponse? =
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. */
fun saveDetail(checkingId: String, detail: PaymentDetailResponse) {
detailCache[checkingId] = detail
Log.d(TAG, "PAYMENTS [DETAIL SET] $checkingId")
Timber.d("PAYMENTS [DETAIL SET] $checkingId")
}
/** Upsert PaymentRecord to database. */
suspend fun upsertPayment(payment: PaymentRecord) {
val entity = payment.toEntity(savedAt = System.currentTimeMillis())
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. */
suspend fun getDetailFromDb(checkingId: String): PaymentDetailResponse? {
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(
paid = entity.status == "success",
details = entity.toDomain()
@@ -158,6 +158,6 @@ class PaymentCacheRepository(context: Context) {
suspend fun clearAll() {
detailCache.clear()
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
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.GsonProvider.gson
import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.db.PaymentRecordEntity
@@ -16,7 +16,7 @@ private fun parseCreatedAt(time: String): Long? {
java.time.OffsetDateTime.parse(time).toEpochSecond()
}.getOrNull()
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
}
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.security
import android.content.Context
import android.util.Log
import timber.log.Timber
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.security
import android.util.Log
import timber.log.Timber
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.crypto.tink.StreamingAead
@@ -22,10 +22,10 @@ class CredentialsSerializer(
val result = Credentials.parseFrom(
streamingAead.newDecryptingStream(input, AAD)
)
Log.d(TAG, "Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}")
Timber.d("Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}")
result
} 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)
}
}
@@ -37,12 +37,12 @@ class CredentialsSerializer(
val encryptingStream = streamingAead.newEncryptingStream(output, AAD)
t.writeTo(encryptingStream) // returns Int — discarded by Unit return type
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()}, " +
"hasAdminKey: ${t.adminKey.isNotBlank()}, " +
"hasBaseUrl: ${t.baseUrl.isNotBlank()}")
} catch (e: Exception) {
Log.e(TAG, "Encryption/write failed", e)
Timber.e("Encryption/write failed", e)
throw e
}
}
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.security
import android.content.Context
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.util.WalletConstants
import com.google.crypto.tink.StreamingAead
import com.google.crypto.tink.integration.android.AndroidKeysetManager
@@ -21,7 +21,7 @@ object CredentialsTinkManager {
init {
// Register all StreamingAead primitives once at class load time
StreamingAeadConfig.register()
Log.d(TAG, "StreamingAeadConfig registered")
Timber.d("StreamingAeadConfig registered")
}
fun getOrCreateStreamingAead(context: Context): StreamingAead {
@@ -33,12 +33,12 @@ object CredentialsTinkManager {
.build()
.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 {
Log.i(TAG, "StreamingAead primitive acquired successfully")
Timber.i("StreamingAead primitive acquired successfully")
}
} 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
}
}
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.security
import android.content.Context
import android.util.Log
import timber.log.Timber
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@@ -50,7 +50,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
invoiceKey: 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}")
dataStore.updateData { current ->
current.toBuilder()
@@ -59,7 +59,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
.setAdminKey(adminKey)
.build()
}
Log.i(TAG, "Credentials saved and DataStore updated")
Timber.i("Credentials saved and DataStore updated")
}
override suspend fun saveBaseUrl(baseUrl: String) {
dataStore.updateData { current ->
@@ -67,7 +67,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
.setBaseUrl(baseUrl)
.build()
}
Log.i(TAG, "BaseUrl saved and DataStore updated")
Timber.i("BaseUrl saved and DataStore updated")
}
override suspend fun saveInvoiceKey(invoiceKey: String) {
@@ -76,7 +76,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
.setInvoiceKey(invoiceKey)
.build()
}
Log.i(TAG, "InvoiceKey saved and DataStore updated")
Timber.i("InvoiceKey saved and DataStore updated")
}
override suspend fun saveAdminKey(adminKey: String) {
@@ -85,7 +85,7 @@ class EncryptedSecretStore(context: Context) : SecretStore {
.setAdminKey(adminKey)
.build()
}
Log.i(TAG, "AdminKey saved and DataStore updated")
Timber.i("AdminKey saved and DataStore updated")
}
override suspend fun setOnboarded() {
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.service
import android.content.Context
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.NodeAliasClient as NodeAliasApiClient
import com.bitcointxoko.gudariwallet.data.NodeAliasCacheRepository
import kotlinx.coroutines.flow.MutableStateFlow
@@ -26,14 +26,14 @@ class NodeAliasService(
suspend fun resolve(pubkey: String): String? {
// 1. Room cache hit
repo.get(pubkey)?.let {
Log.d(TAG, "ALIAS [CACHE HIT ] $pubkey\"$it\"")
Timber.d("ALIAS [CACHE HIT ] $pubkey\"$it\"")
return it
}
// 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 {
Log.w(TAG, "ALIAS [NOT FOUND ] $pubkey")
Timber.w("ALIAS [NOT FOUND ] $pubkey")
return null
}
@@ -43,7 +43,7 @@ class NodeAliasService(
// 4. Publish to StateFlow
_aliases.value = _aliases.value + (pubkey to alias)
Log.d(TAG, "ALIAS [FETCHED ] $pubkey\"$alias\"")
Timber.d("ALIAS [FETCHED ] $pubkey\"$alias\"")
return alias
}
@@ -6,7 +6,7 @@ import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.GsonProvider
import com.bitcointxoko.gudariwallet.api.LNbitsClient
import com.bitcointxoko.gudariwallet.api.PaymentRecord
@@ -124,7 +124,7 @@ class WalletNotificationService : Service() {
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Service destroyed — closing WebSocket")
Timber.d("Service destroyed — closing WebSocket")
reconnectJob?.cancel()
webSocket?.close(WalletConstants.WS_CLOSE_NORMAL, "Service stopped")
webSocket = null
@@ -143,7 +143,7 @@ class WalletNotificationService : Service() {
val baseUrl = secrets.baseUrl()
if (invoiceKey.isBlank() || baseUrl.isBlank()) {
Log.e(TAG, "Credentials not available — cannot open WebSocket")
Timber.e("Credentials not available — cannot open WebSocket")
stopSelf()
return
}
@@ -153,36 +153,36 @@ class WalletNotificationService : Service() {
.replace("http://", "ws://")
.trimEnd('/') + "/api/v1/ws/${invoiceKey}"
Log.d(TAG, "Opening WebSocket: $wsUrl")
Timber.d("Opening WebSocket: $wsUrl")
val request = Request.Builder().url(wsUrl).build()
webSocket = LNbitsClient.httpClient.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(TAG, "WebSocket connected")
Timber.d("WebSocket connected")
// Reset backoff on successful connection
reconnectAttempts = 0
currentBackoffMs = WalletConstants.WS_INITIAL_BACKOFF_MS
}
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "WebSocket message: $text")
Timber.d("WebSocket message: $text")
handleMessage(text)
}
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()
}
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)
}
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)
if (code != WalletConstants.WS_CLOSE_NORMAL) {
scheduleReconnect()
@@ -257,7 +257,7 @@ class WalletNotificationService : Service() {
private fun scheduleReconnect() {
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
// (e.g. when the app is brought to foreground and MainActivity calls start())
return
@@ -267,7 +267,7 @@ class WalletNotificationService : Service() {
reconnectAttempts++
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 = serviceScope.launch {
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.sync
import android.content.Context
import android.util.Log
import timber.log.Timber
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
@@ -29,11 +29,11 @@ class HistoricalSyncWorker(
override suspend fun doWork(): Result {
// Guard: already completed — safe if WorkManager ever retries a success
if (syncStore.isComplete()) {
Log.d(TAG, "Already complete — skipping")
Timber.d("Already complete — skipping")
return Result.success()
}
Log.d(TAG, "Starting historical payment sync")
Timber.d("Starting historical payment sync")
var offset = 0
var total = -1
@@ -41,14 +41,14 @@ class HistoricalSyncWorker(
val page = runCatching {
repo.getPaymentsPaginated(offset = offset, limit = PAGE_SIZE)
}.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
}
// First page: learn the real total from the server
if (total == -1) {
total = page.total
Log.d(TAG, "Server reports $total total payments")
Timber.d("Server reports $total total payments")
if (total == 0) {
syncStore.markComplete()
return Result.success()
@@ -59,7 +59,7 @@ class HistoricalSyncWorker(
paymentCache.mergePayments(page.data)
offset += page.data.size
Log.d(TAG, "Progress: $offset / $total")
Timber.d("Progress: $offset / $total")
setProgress(workDataOf(
KEY_FETCHED to offset,
KEY_TOTAL to total
@@ -70,7 +70,7 @@ class HistoricalSyncWorker(
}
syncStore.markComplete()
Log.d(TAG, "Historical sync complete — $offset payments stored")
Timber.d("Historical sync complete — $offset payments stored")
NotificationHelper.notifySyncComplete(applicationContext, offset)
return Result.success()
}
@@ -1,7 +1,7 @@
package com.bitcointxoko.gudariwallet.ui
import android.content.ClipboardManager
import android.util.Log
import timber.log.Timber
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
@@ -164,7 +164,7 @@ fun WalletScreen(
LaunchedEffect(Unit) {
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) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
@@ -178,7 +178,7 @@ fun WalletScreen(
val intentUri = pendingPaymentUri.value
LaunchedEffect(intentUri) {
if (intentUri != null) {
Log.d("WalletScreen", "INTENT [URI] deep link received: $intentUri")
Timber.d("INTENT [URI] deep link received: $intentUri")
pendingPaymentUri.value = null
vm.scan(intentUri, sendStrings)
navController.navigate(TabItem.Send.route) {
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui
import android.util.Log
import timber.log.Timber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
@@ -144,21 +144,21 @@ class WalletViewModel(
// via SharingStarted.Eagerly — this coroutine only does the network refresh.
viewModelScope.launch {
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
}
runCatching { repo.getLightningAddress() }
.onSuccess { address ->
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)
// StateFlow updates automatically via the DataStore flow
} 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 ->
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
}
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.balance
import android.util.Log
import timber.log.Timber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
@@ -45,7 +45,7 @@ class BalanceViewModel(
val savedAt = balancePrefs.getBalanceSavedAt()
val ageMs = System.currentTimeMillis() - savedAt
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(
sats = savedSats,
isRefreshing = false,
@@ -53,9 +53,9 @@ class BalanceViewModel(
)
} else {
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 {
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
refreshBalance()
@@ -67,7 +67,7 @@ class BalanceViewModel(
fun refreshBalance() {
val current = _balanceState.value
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)
}
@@ -77,9 +77,9 @@ class BalanceViewModel(
val sats = balance.sats
val prev = (_balanceState.value as? BalanceState.Success)?.sats
when {
prev == null -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — cold load complete")
prev == sats -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — matches cache, no change")
else -> Log.d(TAG, "BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})")
prev == null -> Timber.d("BALANCE [NETWORK ] $sats sats — cold load complete")
prev == sats -> Timber.d("BALANCE [NETWORK ] $sats sats — matches cache, no change")
else -> Timber.d("BALANCE [NETWORK ] $sats sats — was $prev sats (diff ${sats - prev})")
}
_balanceState.value = BalanceState.Success(
sats = sats,
@@ -91,13 +91,13 @@ class BalanceViewModel(
onBalanceRefreshed?.invoke()
}
.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
?: balancePrefs.getSavedBalanceSats().takeIf { it >= 0 }
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 {
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(
message = e.message ?: "Failed to load balance",
@@ -115,11 +115,11 @@ class BalanceViewModel(
private fun observePaymentEvents() {
viewModelScope.launch {
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) {
// 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
} else {
// Fallback: server didn't send balance, apply delta optimistically
@@ -127,7 +127,7 @@ class BalanceViewModel(
if (current is BalanceState.Success) {
val delta = if (event.isOutgoing) -event.amountSats else event.amountSats
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
} else null
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.clipboard
import android.util.Log
import timber.log.Timber
import androidx.lifecycle.ViewModel
import com.bitcointxoko.gudariwallet.ui.ClipboardOfferState
import com.bitcointxoko.gudariwallet.util.SendInputDetector
@@ -23,7 +23,7 @@ class ClipboardViewModel(
val clipboardOfferState: StateFlow<ClipboardOfferState> = _clipboardOfferState
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
// ── Deduplicate: don't re-offer the same string twice ─────────────────
@@ -32,7 +32,7 @@ class ClipboardViewModel(
// ── Guard: ignore our own invoice ──────────────────────────────────────
val invoice = ownInvoice()
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
return
}
@@ -40,7 +40,7 @@ class ClipboardViewModel(
// ── Guard: ignore our own lightning address ────────────────────────────
val address = ownAddress()
if (address != null && trimmed.equals(address, ignoreCase = true)) {
Log.d(TAG, "Skipping own lightning address")
Timber.d("Skipping own lightning address")
lastOfferedClipboard = trimmed
return
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.fiat
import android.util.Log
import timber.log.Timber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
@@ -57,7 +57,7 @@ class FiatViewModel(
init {
viewModelScope.launch {
_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 ->
if (state is BalanceState.Success && !state.isRefreshing) {
fetchAndApplyFiatRate()
@@ -72,7 +72,7 @@ class FiatViewModel(
viewModelScope.launch {
balancePrefs.setSelectedCurrency(currency)
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) {
viewModelScope.launch { fetchAndApplyFiatRate() }
} else {
@@ -109,8 +109,8 @@ class FiatViewModel(
?: run {
runCatching { repo.getFiatRate(currency) }
.getOrElse { e ->
Log.w(TAG, "FIAT [RATE ERROR ] $currency${e.message}")
Log.w(TAG, "FIAT [RATE MISS ] $currency — no fallback available, suppressing fiat display")
Timber.w("FIAT [RATE ERROR ] $currency${e.message}")
Timber.w("FIAT [RATE MISS ] $currency — no fallback available, suppressing fiat display")
return
}
.also { fresh -> fiatCache.putRate(currency, fresh) }
@@ -118,7 +118,7 @@ class FiatViewModel(
val currentSats = (balanceVm.balanceState.value as? BalanceState.Success)?.sats ?: return
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)
}
@@ -131,7 +131,7 @@ class FiatViewModel(
private suspend fun fetchAndCacheCurrencies(): List<String> {
return runCatching { repo.getSupportedCurrencies() }
.getOrElse { e ->
Log.w(TAG, "FIAT [CURRENCIES] network fetch failed — ${e.message}")
Timber.w("FIAT [CURRENCIES] network fetch failed — ${e.message}")
emptyList()
}
.also { list -> fiatCache.putCurrencies(list) }
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.history
import android.util.Log
import timber.log.Timber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@@ -161,10 +161,10 @@ class HistoryViewModel(
) { _, f -> f }
.collect { f ->
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
} 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} " +
"amt=${f.minAmountSat}${f.maxAmountSat} " +
"date=${f.minCreatedAt}${f.maxCreatedAt}")
@@ -174,7 +174,7 @@ class HistoryViewModel(
}
viewModelScope.launch {
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()
}
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.history
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -22,15 +22,15 @@ class PaymentDetailDelegate(
fun load(checkingId: String, record: PaymentRecord? = null) {
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)
return
}
_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)
} else {
Log.d(TAG, "DETAIL [LOD] $checkingId — no record, showing spinner")
Timber.d("DETAIL [LOD] $checkingId — no record, showing spinner")
DetailState.Loading
}
scope.launch {
@@ -38,17 +38,17 @@ class PaymentDetailDelegate(
if (fromDb != null) {
paymentCache.saveDetail(checkingId, 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
}
runCatching { repo.getPaymentDetail(checkingId) }
.onSuccess { detail ->
Log.d(TAG, "DETAIL [NET] $checkingId — fetched from network")
Timber.d("DETAIL [NET] $checkingId — fetched from network")
paymentCache.saveDetail(checkingId, detail)
_state.value = DetailState.Success(detail)
}
.onFailure { e ->
Log.w(TAG, "DETAIL [ERR] $checkingId${e.message}")
Timber.w("DETAIL [ERR] $checkingId${e.message}")
if (_state.value !is DetailState.Partial) {
_state.value = DetailState.Error(e.message ?: "Failed to load payment")
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.history
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
@@ -21,7 +21,7 @@ class PaymentEnricher(
val candidates = payments.filter { it.isOutgoing }
if (candidates.isEmpty()) return
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)")
if (toEnrich.isEmpty()) return
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.history
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -37,7 +37,7 @@ class PaymentSyncManager(
limit = WalletConstants.PAYMENTS_PAGE_SIZE
)
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
mutableState.value = HistoryState.Success(
payments = cached.distinctBy { it.paymentHash },
@@ -45,7 +45,7 @@ class PaymentSyncManager(
isRefreshing = true
)
} else {
Log.d(TAG, "PAYMENTS [INIT ] No cache — cold load")
Timber.d("PAYMENTS [INIT ] No cache — cold load")
}
syncNewPayments()
}
@@ -78,7 +78,7 @@ class PaymentSyncManager(
limit = 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 }
currentOffset += fromDb.size
val newState = HistoryState.Success(
@@ -91,7 +91,7 @@ class PaymentSyncManager(
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 {
repo.getPayments(offset = currentOffset, limit = WalletConstants.PAYMENTS_PAGE_SIZE)
}.onSuccess { newPage ->
@@ -106,7 +106,7 @@ class PaymentSyncManager(
mutableState.value = newState
onNewPayments(newState.payments)
}.onFailure { e ->
Log.w(TAG, "PAYMENTS [MORE ERR ] ${e.message}")
Timber.w("PAYMENTS [MORE ERR ] ${e.message}")
mutableState.value = current.copy(isRefreshing = false)
}
}
@@ -119,11 +119,11 @@ class PaymentSyncManager(
var totalNew = 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) {
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
}
@@ -132,10 +132,10 @@ class PaymentSyncManager(
}.getOrElse { e ->
val current = mutableState.value
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)
} 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")
}
return
@@ -152,14 +152,14 @@ class PaymentSyncManager(
paymentCache.mergePayments(toMerge)
totalNew += toMerge.size
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 reachedEnd = page.size < WalletConstants.PAYMENTS_PAGE_SIZE
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")
break
}
@@ -2,7 +2,7 @@ package com.bitcointxoko.gudariwallet.ui.nfc
import android.nfc.cardemulation.HostApduService
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
@@ -71,7 +71,7 @@ class NdefHceService : HostApduService() {
private var selectedFileId: ByteArray? = null
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
@@ -85,7 +85,7 @@ class NdefHceService : HostApduService() {
ins == 0xB0.toByte() -> handleReadBinary(apdu)
else -> SW_UNKNOWN
}.also { Log.d(TAG, "APDU OUT: ${it.toHex()}") }
}.also { Timber.d("APDU OUT: ${it.toHex()}") }
}
private fun handleSelect(apdu: ByteArray): ByteArray {
@@ -97,21 +97,21 @@ class NdefHceService : HostApduService() {
return when {
data.contentEquals(NDEF_AID) -> {
selectedFileId = null // app selected, no file yet
Log.d(TAG, "SELECT NDEF Application OK")
Timber.d("SELECT NDEF Application OK")
SW_OK
}
data.contentEquals(CC_FILE_ID) -> {
selectedFileId = CC_FILE_ID
Log.d(TAG, "SELECT CC file OK")
Timber.d("SELECT CC file OK")
SW_OK
}
data.contentEquals(NDEF_FILE_ID) -> {
selectedFileId = NDEF_FILE_ID
Log.d(TAG, "SELECT NDEF file OK")
Timber.d("SELECT NDEF file OK")
SW_OK
}
else -> {
Log.w(TAG, "SELECT unknown file: ${data.toHex()}")
Timber.w("SELECT unknown file: ${data.toHex()}")
SW_FILE_NOT_FOUND
}
}
@@ -127,7 +127,7 @@ class NdefHceService : HostApduService() {
CC_FILE
selectedFileId != null && selectedFileId!!.contentEquals(NDEF_FILE_ID) -> {
ndefFileBytes ?: run {
Log.w(TAG, "READ BINARY: no NDEF data loaded")
Timber.w("READ BINARY: no NDEF data loaded")
return SW_FILE_NOT_FOUND
}
}
@@ -141,7 +141,7 @@ class NdefHceService : HostApduService() {
}
override fun onDeactivated(reason: Int) {
Log.d(TAG, "HCE deactivated, reason=$reason")
Timber.d("HCE deactivated, reason=$reason")
selectedFileId = null
}
@@ -8,7 +8,7 @@ import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.nfc.tech.Ndef
import android.os.Build
import android.util.Log
import timber.log.Timber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.ui.NfcOfferState
@@ -33,7 +33,7 @@ class NfcViewModel : ViewModel() {
* Builds the NDEF file bytes and loads them into NdefHceService.
*/
fun startEmulating(uri: String) {
Log.d(TAG, "startEmulating: $uri")
Timber.d("startEmulating: $uri")
NdefHceService.ndefFileBytes = buildNdefFileBytes(uri)
_isNfcEmulating.value = true
}
@@ -42,7 +42,7 @@ class NfcViewModel : ViewModel() {
* Stop emulating clears the NDEF payload from the HCE service.
*/
fun stopEmulating() {
Log.d(TAG, "stopEmulating")
Timber.d("stopEmulating")
NdefHceService.ndefFileBytes = null
_isNfcEmulating.value = false
}
@@ -63,7 +63,7 @@ class NfcViewModel : ViewModel() {
* recognisable payment URI.
*/
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
readNdefText(tag)
}
@@ -87,7 +87,7 @@ class NfcViewModel : ViewModel() {
val message = rawMessages[0] as NdefMessage
val uri = extractUri(message)
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))
return
}
@@ -127,14 +127,14 @@ class NfcViewModel : ViewModel() {
extractUri(msg)
}
} catch (e: Exception) {
Log.e(TAG, "readNdefText error: ${e.message}")
Timber.e("readNdefText error: ${e.message}")
null
}
if (uri != null) {
Log.d(TAG, "readNdefText: uri=$uri")
Timber.d("readNdefText: uri=$uri")
_nfcOfferState.value = NfcOfferState.Detected(uri, SendInputDetector.detect(uri))
} 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
import android.util.Log
import timber.log.Timber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -32,7 +32,7 @@ class ReceiveViewModel(
fun onIncomingPayment(event: IncomingPaymentEvent) {
val rs = _receiveState.value
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(
amountSats = event.amountSats,
memo = event.memo,
@@ -72,7 +72,7 @@ class ReceiveViewModel(
)
}
.onFailure { e ->
Log.e(TAG, "RECEIVE [CREATE INVOICE ERROR] ${e.message}")
Timber.e("RECEIVE [CREATE INVOICE ERROR] ${e.message}")
_receiveState.value = ReceiveState.Error(
parseApiError(e, failedToCreateInvoice) // ← localised
)
@@ -100,7 +100,7 @@ class ReceiveViewModel(
lnurl = lnurl
)
_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) {
val e = invoiceResult.exceptionOrNull()
?: Exception(failedToCreateInvoice)
Log.e(TAG, "LNURL-WITHDRAW [INVOICE ERROR] ${e.message}")
Timber.e("LNURL-WITHDRAW [INVOICE ERROR] ${e.message}")
_receiveState.value = ReceiveState.Error(
parseApiError(e, failedToCreateInvoice) // ← localised
)
return@launch
}
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) }
.onSuccess { response ->
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(
bolt11 = invoice.bolt11,
paymentHash = invoice.paymentHash,
@@ -147,12 +147,12 @@ class ReceiveViewModel(
} else {
val reason = response.reason?.ifBlank { null }
?: withdrawRejected // ← localised
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK ERR] $reason")
Timber.w("LNURL-WITHDRAW [CALLBACK ERR] $reason")
_receiveState.value = ReceiveState.Error(reason)
}
}
.onFailure { e ->
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}")
Timber.w("LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}")
_receiveState.value = ReceiveState.Error(
parseApiError(e, withdrawFailed) // ← localised
)
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -28,12 +28,12 @@ class LnurlPayer(
// ── 2. Terminal error — don't retry
val initialError = initial.exceptionOrNull() ?: Exception("Payment failed")
if (isTerminalPayError(initialError)) {
Log.w(TAG, "LNURL [PAY TERMINAL] ${initialError.message}")
Timber.w("LNURL [PAY TERMINAL] ${initialError.message}")
return Result.failure(initialError)
}
// ── 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)
val rescanResult = scanner.rescan(lnurl)
@@ -44,7 +44,7 @@ class LnurlPayer(
val protocolError = lnurlProtocolError(freshRaw)
if (protocolError != null) {
Log.w(TAG, "LNURL [RESCAN PROTO] $protocolError")
Timber.w("LNURL [RESCAN PROTO] $protocolError")
return Result.failure(Exception(protocolError))
}
cache.put(lnurl, freshRaw)
@@ -53,7 +53,7 @@ class LnurlPayer(
val retry = tryWithFallback(freshRaw, lnurl, amountMsat, comment, "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"))
}
@@ -66,17 +66,17 @@ class LnurlPayer(
): Result<String> {
val direct = runCatching { repo.payLnurlDirect(rawRes, amountMsat, comment) }
if (direct.isSuccess) {
Log.d(TAG, "LNURL [PAY $label DIRECT] success")
Timber.d("LNURL [PAY $label DIRECT] success")
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) }
if (proxy.isSuccess) {
Log.d(TAG, "LNURL [PAY $label PROXY] success")
Timber.d("LNURL [PAY $label PROXY] success")
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"))
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
import com.bitcointxoko.gudariwallet.data.WalletRepository
@@ -18,25 +18,25 @@ class LnurlScanner(
// 1. Cache hit
val cached = lnurlCache.get(lnurl)
if (cached != null) {
Log.d(TAG, "LNURL [SCAN CACHE ] $lnurl")
Timber.d("LNURL [SCAN CACHE ] $lnurl")
return Result.success(LnurlScanResult(cached, fromCache = true))
}
// 2. Direct
val direct = runCatching { repo.scanLnurlDirect(lnurl) }
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))
}
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
val proxy = runCatching { repo.scanLnurl(lnurl) }
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))
}
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"))
}
@@ -44,15 +44,15 @@ class LnurlScanner(
suspend fun rescan(lnurl: String): Result<LnurlScanResponse> {
val direct = runCatching { repo.scanLnurlDirect(lnurl) }
if (direct.isSuccess) {
Log.d(TAG, "LNURL [RESCAN DIRECT] success")
Timber.d("LNURL [RESCAN DIRECT] success")
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) }
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"))
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.data.WalletRepository
import kotlinx.coroutines.delay
@@ -25,21 +25,21 @@ class PaymentPoller(private val repo: WalletRepository) {
when {
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)
}
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
}
else -> {
Log.w(TAG, "HOLD INVOICE [POLL $attempt] payment failed — $paymentHash")
Timber.w("HOLD INVOICE [POLL $attempt] payment failed — $paymentHash")
return PollResult.Failed
}
}
}
Log.w(TAG, "HOLD INVOICE [POLL TIMEOUT] $paymentHash")
Timber.w("HOLD INVOICE [POLL TIMEOUT] $paymentHash")
return PollResult.TimedOut
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.ui.send
import android.util.Log
import timber.log.Timber
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
@@ -78,7 +78,7 @@ class SendViewModel(
fun scan(rawInput: String, strings: SendStrings) {
val normalized = SendInputDetector.normalize(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) {
SendInputType.Unknown -> {
@@ -100,11 +100,11 @@ class SendViewModel(
viewModelScope.launch {
val decoded = _sendState.value as? SendState.Bolt11Decoded
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
}
_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) }
.onSuccess { response ->
handlePaymentResult(
@@ -118,7 +118,7 @@ class SendViewModel(
)
}
.onFailure { e ->
Log.e(TAG, "SEND [PAY BOLT11 ERROR] ${e.message}")
Timber.e("SEND [PAY BOLT11 ERROR] ${e.message}")
_sendState.value = SendState.Error(
parseApiError(e, strings.paymentFailed) // ← "Payment failed"
)
@@ -238,7 +238,7 @@ class SendViewModel(
val bolt11 = runCatching {
repo.fetchLnurlCallbackInvoice(rawRes, amountMsat, comment)
}.getOrElse { e ->
Log.e(TAG, "LNURL [FETCH INVOICE ERR] ${e.message}")
Timber.e("LNURL [FETCH INVOICE ERR] ${e.message}")
_sendState.value = SendState.Error(
parseApiError(e, strings.couldNotFetchInvoice) // ← "Could not fetch invoice from recipient"
)
@@ -248,7 +248,7 @@ class SendViewModel(
val decoded = runCatching {
repo.decodeBolt11(bolt11)
}.getOrElse { e ->
Log.e(TAG, "LNURL [DECODE ERR] ${e.message}")
Timber.e("LNURL [DECODE ERR] ${e.message}")
_sendState.value = SendState.Error(
parseApiError(e, strings.couldNotDecodeInvoice) // ← "Could not decode invoice"
)
@@ -260,7 +260,7 @@ class SendViewModel(
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}")
_sendState.value = SendState.LnurlInvoiceReady(
@@ -315,7 +315,7 @@ class SendViewModel(
)
return@launch
}
Log.w(TAG, "LNURL [PAY INVOICE DIRECT FAIL] " +
Timber.w("LNURL [PAY INVOICE DIRECT FAIL] " +
"${directResult.exceptionOrNull()?.message} — falling back to payLnurl")
executeLnurlPayment(state.rawRes, state.lnurl, state.amountMsat, state.comment, strings)
}
@@ -347,12 +347,12 @@ class SendViewModel(
return
}
val httpsUrl = "https://" + rawInput.trim().substringAfter("://")
Log.d(TAG, "LUD17 [FETCH] $httpsUrl")
Timber.d("LUD17 [FETCH] $httpsUrl")
_sendState.value = SendState.Scanning
viewModelScope.launch {
runCatching { repo.fetchLnurlDirect(httpsUrl) }
.onSuccess { raw ->
Log.d(TAG, "LUD17 [RESPONSE] tag=${raw.tag}")
Timber.d("LUD17 [RESPONSE] tag=${raw.tag}")
when (raw.tag) {
"withdrawRequest" -> {emitWithdrawScanned(raw, httpsUrl)}
"payRequest" -> {
@@ -365,7 +365,7 @@ class SendViewModel(
}
}
.onFailure { e ->
Log.e(TAG, "LUD17 [ERROR] ${e.message}")
Timber.e("LUD17 [ERROR] ${e.message}")
_sendState.value = SendState.Error(
parseApiError(e, strings.couldNotResolveAddress) // ← "Could not resolve address"
)
@@ -410,7 +410,7 @@ class SendViewModel(
)
}
.onFailure { e ->
Log.e(TAG, "SEND [DECODE ERROR] ${e.message}")
Timber.e("SEND [DECODE ERROR] ${e.message}")
_sendState.value = SendState.Error(
parseApiError(e, strings.couldNotDecodeInvoice) // ← "Could not decode invoice"
)
@@ -428,7 +428,7 @@ class SendViewModel(
val protocolError = lnurlProtocolError(raw)
if (protocolError != null) {
Log.w(TAG, "LNURL [SCAN PROTO ERR] $protocolError")
Timber.w("LNURL [SCAN PROTO ERR] $protocolError")
_sendState.value = SendState.Error(protocolError)
return@launch
}
@@ -2,7 +2,7 @@ package com.bitcointxoko.gudariwallet.util
import android.content.Context
import android.content.ContextWrapper
import android.util.Log
import timber.log.Timber
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
@@ -61,7 +61,7 @@ object BiometricHelper {
if (code != BiometricPrompt.ERROR_USER_CANCELED &&
code != BiometricPrompt.ERROR_NEGATIVE_BUTTON
) {
Log.e(TAG, "Biometric error $code: $msg")
Timber.e("Biometric error $code: $msg")
onError(code, msg)
}
}
@@ -1,6 +1,6 @@
package com.bitcointxoko.gudariwallet.util
import android.util.Log
import timber.log.Timber
import com.bitcointxoko.gudariwallet.api.RouteHintHop
import com.bitcointxoko.gudariwallet.data.DecodedBolt11
import fr.acinq.secp256k1.Secp256k1
@@ -59,7 +59,7 @@ object Bolt11Decoder {
// ── 1. Split HRP / data ───────────────────────────────────────────────
val sepIdx = lower.lastIndexOf('1')
if (sepIdx < 1) {
Log.w(TAG, "No bech32 separator found")
Timber.w("No bech32 separator found")
return null
}
@@ -67,21 +67,21 @@ object Bolt11Decoder {
val dataChars = lower.substring(sepIdx + 1)
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
}
// ── 2. Parse HRP ──────────────────────────────────────────────────────
val prefix = KNOWN_PREFIXES.firstOrNull { hrp.startsWith(it) }
?: run {
Log.w(TAG, "Unknown HRP prefix: $hrp")
Timber.w("Unknown HRP prefix: $hrp")
return null
}
val amountStr = hrp.removePrefix(prefix)
val amountMsat = parseAmountMsat(amountStr)
?: run {
Log.w(TAG, "Could not parse amount from HRP: '$amountStr'")
Timber.w("Could not parse amount from HRP: '$amountStr'")
return null
}
@@ -98,7 +98,7 @@ object Bolt11Decoder {
val groups = payload.map { c ->
val idx = CHARSET.indexOf(c)
if (idx < 0) {
Log.w(TAG, "Invalid bech32 character: '$c'")
Timber.w("Invalid bech32 character: '$c'")
return null
}
idx
@@ -108,7 +108,7 @@ object Bolt11Decoder {
val sigGroups = groups.takeLast(SIGNATURE_GROUPS)
val signedGroups = groups.dropLast(SIGNATURE_GROUPS)
if (signedGroups.size < 7) {
Log.w(TAG, "Data part too short for timestamp")
Timber.w("Data part too short for timestamp")
return null
}
@@ -139,7 +139,7 @@ object Bolt11Decoder {
pos += 3
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
}
@@ -152,7 +152,7 @@ object Bolt11Decoder {
TAG_PAYMENT_HASH -> {
// Fixed: 52 groups × 5 = 260 bits → 32 bytes + 4 padding bits
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
}
val bytes = convertBits(fieldGroups, 5, 8, false) ?: continue
@@ -166,7 +166,7 @@ object Bolt11Decoder {
TAG_PAYEE_PUBKEY -> {
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
}
val bytes = convertBits(fieldGroups, 5, 8, false) ?: continue
@@ -185,7 +185,7 @@ object Bolt11Decoder {
TAG_FALLBACK_ADDR -> {
fallbackAddress = decodeFallbackAddress(fieldGroups, addrHrp)
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 ───────────
if (payee == null) {
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 ─────────────
if (paymentHash == null) {
Log.w(TAG, "Invoice missing required 'p' (payment hash) field")
Timber.w("Invoice missing required 'p' (payment hash) field")
return null
}
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}, " +
"createdAt: $createdAt, expiresAt: $expiresAt, " +
"fallback: $fallbackAddress, paymentHash: $paymentHash")
@@ -287,7 +287,7 @@ object Bolt11Decoder {
encodeBech32Address(addrHrp, witnessVersion = 0, dataGroups)
}
else -> {
Log.w(TAG, "Unknown fallback address version: $version")
Timber.w("Unknown fallback address version: $version")
null
}
}
@@ -372,20 +372,20 @@ object Bolt11Decoder {
val sigBytes = convertBits(sigGroups, 5, 8, false)?.toByteArray() ?: return null
if (sigBytes.size != 65) {
Log.w(TAG, "Unexpected signature length: ${sigBytes.size}")
Timber.w("Unexpected signature length: ${sigBytes.size}")
return null
}
val compactSig = sigBytes.copyOfRange(0, 64)
val recid = sigBytes[64].toInt()
if (recid !in 0..3) {
Log.w(TAG, "Invalid recovery id: $recid")
Timber.w("Invalid recovery id: $recid")
return null
}
val uncompressed = Secp256k1.ecdsaRecover(compactSig, message, recid)
Secp256k1.pubKeyCompress(uncompressed).toHex()
}.getOrElse {
Log.w(TAG, "Payee recovery failed: ${it.message}")
Timber.w("Payee recovery failed: ${it.message}")
null
}
+2
View File
@@ -33,6 +33,7 @@ work = "2.9.1"
benchmark = "1.4.1"
runner = "1.5.2"
androidx-test-ext = "1.2.1"
timber = "5.0.1"
[libraries]
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-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
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]
android-application = { id = "com.android.application", version.ref = "agp" }