refactor: move logs to timber
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user