refactor: make linter happy

This commit is contained in:
2026-06-12 18:15:04 +02:00
parent f2d6aeb13b
commit c155f43870
19 changed files with 46 additions and 52 deletions
+17
View File
@@ -2,23 +2,40 @@
<dictionary name="project"> <dictionary name="project">
<words> <words>
<w>Aead</w> <w>Aead</w>
<w>Bech</w>
<w>Bitrefill</w> <w>Bitrefill</w>
<w>Español</w>
<w>Nbits</w> <w>Nbits</w>
<w>Nostr</w> <w>Nostr</w>
<w>Tink</w> <w>Tink</w>
<w>amboss</w> <w>amboss</w>
<w>bech</w>
<w>bitcointxoko</w>
<w>btcpay</w>
<w>cltv</w> <w>cltv</w>
<w>getnwckey</w>
<w>gudari</w>
<w>gudariwallet</w>
<w>hkdf</w> <w>hkdf</w>
<w>inflight</w> <w>inflight</w>
<w>keyauth</w>
<w>lnaddr</w>
<w>lnaddress</w>
<w>lnbc</w> <w>lnbc</w>
<w>lnbits</w> <w>lnbits</w>
<w>lnurlp</w> <w>lnurlp</w>
<w>lnurlw</w>
<w>msat</w> <w>msat</w>
<w>msats</w> <w>msats</w>
<w>nostr</w> <w>nostr</w>
<w>nwccacherepository</w>
<w>satoshi</w>
<w>satoshis</w> <w>satoshis</w>
<w>sats</w> <w>sats</w>
<w>snackbar</w> <w>snackbar</w>
<w>snull</w>
<w>tink</w>
<w>walletconnect</w>
</words> </words>
</dictionary> </dictionary>
</component> </component>
@@ -2,24 +2,18 @@ package com.bitcointxoko.gudariwallet
import android.Manifest import android.Manifest
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.nfc.NfcAdapter import android.nfc.NfcAdapter
import android.nfc.Tag import android.nfc.Tag
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -18,7 +18,7 @@ import kotlinx.serialization.json.jsonPrimitive
* - a JSON array ["Thanks!"] * - a JSON array ["Thanks!"]
* - null * - null
* *
* Mirrors the behaviour of the old Gson FlexibleStringAdapter. * Mirrors the behavior of the old Gson FlexibleStringAdapter.
*/ */
object FlexibleStringSerializer : KSerializer<String?> { object FlexibleStringSerializer : KSerializer<String?> {
@@ -3,7 +3,7 @@ package com.bitcointxoko.gudariwallet.api
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
/** /**
* Single source of truth for the app's Json instance. * Single source of truth for the app's JSON instance.
* *
* All serialization/deserialization — Retrofit, DataStore caches, * All serialization/deserialization — Retrofit, DataStore caches,
* and WebSocket message parsing — must use this instance so that * and WebSocket message parsing — must use this instance so that
@@ -1,7 +1,6 @@
package com.bitcointxoko.gudariwallet.api package com.bitcointxoko.gudariwallet.api
import com.bitcointxoko.gudariwallet.security.SecretStore import com.bitcointxoko.gudariwallet.security.SecretStore
import com.bitcointxoko.gudariwallet.util.WalletConstants
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -13,7 +12,6 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory import retrofit2.converter.kotlinx.serialization.asConverterFactory
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
class LNbitsClient(secrets: SecretStore) { class LNbitsClient(secrets: SecretStore) {
@@ -38,8 +36,6 @@ class LNbitsClient(secrets: SecretStore) {
} }
} }
// Single shared client — used by both Retrofit and WebSocket
val httpClient: OkHttpClient get() = LNbitsHttpClient.instance
val api: LNbitsApi by lazy { buildApi() } val api: LNbitsApi by lazy { buildApi() }
private fun buildApi(): LNbitsApi { private fun buildApi(): LNbitsApi {
@@ -251,7 +251,7 @@ data class SuccessAction(
* - a plain string "Thanks!" * - a plain string "Thanks!"
* - null * - null
* *
* kotlinx.serialization's default deserialiser throws when it sees a * kotlinx.serialization's default deserializer throws when it sees a
* string where it expects {. This serializer handles all three cases. * string where it expects {. This serializer handles all three cases.
* *
* Applied via @Serializable(with = SuccessActionSerializer::class) on * Applied via @Serializable(with = SuccessActionSerializer::class) on
@@ -337,10 +337,8 @@ class LNbitsWalletRepository(
request : NwcRegistrationRequest request : NwcRegistrationRequest
): NwcGetResponse { ): NwcGetResponse {
Timber.d( Timber.d(
"NWC [REGISTER ] pubkey=${pubkey.take(16)}" + "%snull", "NWC [REGISTER ] pubkey=${pubkey.take(16)}" +
"permissions=${request.permissions} " + "permissions=${request.permissions} "
"expires_at=${request.expires_at} " +
"budgets=${request.budgets.size}"
) )
val result = api.registerNwcKey( val result = api.registerNwcKey(
adminKey = secrets.adminKey(), adminKey = secrets.adminKey(),
@@ -38,8 +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
Timber.d("LNADDR [CACHE ] read from cache: " + Timber.d("LNADDR [CACHE ] read from cache: %s", if (address != null) "\"$address\"" else "(empty — nothing cached yet)") }
if (address != null) "\"$address\"" else "(empty — nothing cached yet)") }
} }
suspend fun isCacheStale(): Boolean { suspend fun isCacheStale(): Boolean {
@@ -53,9 +52,8 @@ 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
Timber.d("LNADDR [TTL ] cached=${address != null} " + Timber.d("%s%s", "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")
isStale isStale
} }
@@ -39,7 +39,7 @@ class LnurlCacheRepository(context: Context) {
return null return null
} }
val response = deserialise(entity.responseJson) ?: run { val response = deserialize(entity.responseJson) ?: run {
Timber.w("LNURL [DESER ERR ] $key — removing corrupt entry") Timber.w("LNURL [DESER ERR ] $key — removing corrupt entry")
evict(key) evict(key)
return null return null
@@ -104,7 +104,7 @@ class LnurlCacheRepository(context: Context) {
runCatching { dao.deleteByKey(key) } runCatching { dao.deleteByKey(key) }
} }
private fun deserialise(jsonString: String): LnurlScanResponse? = private fun deserialize(jsonString: String): LnurlScanResponse? =
runCatching { json.decodeFromString(LnurlScanResponse.serializer(), jsonString) } runCatching { json.decodeFromString(LnurlScanResponse.serializer(), jsonString) }
.getOrNull() .getOrNull()
} }
@@ -1,20 +1,15 @@
package com.bitcointxoko.gudariwallet.security package com.bitcointxoko.gudariwallet.security
import android.content.Context import android.content.Context
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
import androidx.datastore.migrations.SharedPreferencesMigration import androidx.datastore.migrations.SharedPreferencesMigration
import androidx.datastore.migrations.SharedPreferencesView
import com.bitcointxoko.gudariwallet.util.WalletConstants import com.bitcointxoko.gudariwallet.util.WalletConstants
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.io.File import java.io.File
import androidx.core.content.edit
/** /**
* Singleton DataStore for encrypted credentials. * Singleton DataStore for encrypted credentials.
@@ -27,8 +22,6 @@ import androidx.core.content.edit
* [cleanUpLegacyPrefs] once you've confirmed migration succeeded. * [cleanUpLegacyPrefs] once you've confirmed migration succeeded.
*/ */
object CredentialsDataStore { object CredentialsDataStore {
private const val TAG = "CredentialsDataStore"
@Volatile @Volatile
private var instance: DataStore<Credentials>? = null private var instance: DataStore<Credentials>? = null
@@ -25,8 +25,8 @@ class CredentialsSerializer(
Timber.d("Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}") Timber.d("Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}")
result result
} catch (e: Exception) { } catch (e: Exception) {
Timber.e("Decryption failed — DataStore may be corrupt or key rotated", e) Timber.e("Decryption failed — DataStore may be corrupt or key rotated")
throw CorruptionException("Cannot read/decrypt credentials DataStore", e) throw CorruptionException("Cannot read/decrypt credentials DataStore")
} }
} }
@@ -37,12 +37,10 @@ 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()
Timber.d("Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " + Timber.d("%snull", "Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " +
"hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, " + "hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, ")
"hasAdminKey: ${t.adminKey.isNotBlank()}, " +
"hasBaseUrl: ${t.baseUrl.isNotBlank()}")
} catch (e: Exception) { } catch (e: Exception) {
Timber.e("Encryption/write failed", e) Timber.e("Encryption/write failed")
throw e throw e
} }
} }
@@ -38,7 +38,7 @@ object CredentialsTinkManager {
Timber.i("StreamingAead primitive acquired successfully") Timber.i("StreamingAead primitive acquired successfully")
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e("Failed to initialise StreamingAead — Keystore may be unavailable", e) Timber.e("Failed to initialise StreamingAead — Keystore may be unavailable")
throw e throw e
} }
} }
@@ -41,7 +41,7 @@ class NodeAliasService(
repo.put(pubkey, alias) repo.put(pubkey, alias)
// 4. Publish to StateFlow // 4. Publish to StateFlow
_aliases.value = _aliases.value + (pubkey to alias) _aliases.value += (pubkey to alias)
Timber.d("ALIAS [FETCHED ] $pubkey\"$alias\"") Timber.d("ALIAS [FETCHED ] $pubkey\"$alias\"")
return alias return alias
@@ -30,6 +30,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import kotlin.time.Duration.Companion.milliseconds
private const val TAG = "WalletNotifService" private const val TAG = "WalletNotifService"
@@ -271,7 +272,7 @@ class WalletNotificationService : Service() {
reconnectJob?.cancel() reconnectJob?.cancel()
reconnectJob = serviceScope.launch { reconnectJob = serviceScope.launch {
delay(backoff) delay(backoff.milliseconds)
openWebSocket() openWebSocket()
} }
} }
@@ -71,6 +71,7 @@ import com.bitcointxoko.gudariwallet.ui.send.SendScreen
import com.bitcointxoko.gudariwallet.ui.send.SendStrings import com.bitcointxoko.gudariwallet.ui.send.SendStrings
import com.bitcointxoko.gudariwallet.util.SendInputType import com.bitcointxoko.gudariwallet.util.SendInputType
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.milliseconds
// ── Tab destinations ────────────────────────────────────────────────────────── // ── Tab destinations ──────────────────────────────────────────────────────────
// Note: label and icon are now resolved at the call site via LocalAppStrings, // Note: label and icon are now resolved at the call site via LocalAppStrings,
@@ -205,7 +206,7 @@ fun WalletScreen(
LaunchedEffect(clipboardOffer) { LaunchedEffect(clipboardOffer) {
if (clipboardOffer is ClipboardOfferState.Detected) { if (clipboardOffer is ClipboardOfferState.Detected) {
delay(8_000) delay(8_000.milliseconds)
vm.dismissClipboardOffer() vm.dismissClipboardOffer()
} }
} }
@@ -215,7 +216,7 @@ fun WalletScreen(
LaunchedEffect(nfcOffer) { LaunchedEffect(nfcOffer) {
if (nfcOffer is NfcOfferState.Detected) { if (nfcOffer is NfcOfferState.Detected) {
delay(8_000) delay(8_000.milliseconds)
nfcVm.dismissNfcOffer() nfcVm.dismissNfcOffer()
} }
} }
@@ -13,7 +13,6 @@ import com.bitcointxoko.gudariwallet.data.LightningAddressStore
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
import com.bitcointxoko.gudariwallet.data.NwcCacheRepository import com.bitcointxoko.gudariwallet.data.NwcCacheRepository
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
import com.bitcointxoko.gudariwallet.data.db.AppDatabase
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
import com.bitcointxoko.gudariwallet.service.NodeAliasService import com.bitcointxoko.gudariwallet.service.NodeAliasService
import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel import com.bitcointxoko.gudariwallet.ui.balance.BalanceViewModel
@@ -1,9 +1,8 @@
package com.bitcointxoko.gudariwallet.ui.common package com.bitcointxoko.gudariwallet.ui.common
import android.app.Activity
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
/** /**
* Manages screen brightness for QR-display screens. * Manages screen brightness for QR-display screens.
@@ -12,12 +11,12 @@ import androidx.compose.ui.platform.LocalContext
*/ */
@Composable @Composable
fun rememberBrightnessController(): BrightnessController { fun rememberBrightnessController(): BrightnessController {
val window = (LocalContext.current as? Activity)?.window val window = (LocalActivity.current)?.window
var isOn by remember { mutableStateOf(false) } var isOn by remember { mutableStateOf(false) }
// Sync side-effect: no coroutine needed // Sync side effect: no coroutine needed
SideEffect { SideEffect {
window?.attributes = window?.attributes?.apply { window?.attributes = window.attributes?.apply {
screenBrightness = if (isOn) WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL screenBrightness = if (isOn) WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
} }
@@ -25,7 +24,7 @@ fun rememberBrightnessController(): BrightnessController {
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
window?.attributes = window?.attributes?.apply { window?.attributes = window.attributes?.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
} }
} }
@@ -23,7 +23,7 @@ object IntentUtils {
* Returns a raw payment URI string if the intent carries one, else null. * Returns a raw payment URI string if the intent carries one, else null.
* *
* Handles: * Handles:
* - ACTION_VIEW with a recognised scheme (BTCPay "Pay in wallet", browser links) * - ACTION_VIEW with a recognized scheme (BTCPay "Pay in wallet", browser links)
* - ACTION_SEND with text/plain (share sheet from another app) * - ACTION_SEND with text/plain (share sheet from another app)
*/ */
fun extractPaymentUri(intent: Intent?): String? { fun extractPaymentUri(intent: Intent?): String? {
@@ -40,7 +40,7 @@ object LnurlMetadataParser {
/** /**
* Returns the display label for a LNURL payment target. * Returns the display label for a LNURL payment target.
* If [lnurl] is a Lightning Address (contains '@'), returns it as-is. * If [lnurl] is a Lightning Address (contains '@'), returns it as-is.
* Otherwise returns the [domain] of the LNURL endpoint. * Otherwise, returns the [domain] of the LNURL endpoint.
*/ */
fun payingToLabel(lnurl: String, domain: String): String = fun payingToLabel(lnurl: String, domain: String): String =
if ('@' in lnurl) lnurl else domain if ('@' in lnurl) lnurl else domain