diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/api/NodeAliasService.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/api/NodeAliasClient.kt.kt similarity index 91% rename from app/src/main/java/com/bitcointxoko/gudariwallet/api/NodeAliasService.kt rename to app/src/main/java/com/bitcointxoko/gudariwallet/api/NodeAliasClient.kt.kt index ec634de..c9f4596 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/api/NodeAliasService.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/api/NodeAliasClient.kt.kt @@ -7,10 +7,10 @@ import java.util.concurrent.TimeUnit /** * Resolves a Lightning node pubkey to a human-readable alias. - * Tries 1ml.com first, falls back to mempool.space. + * Tries mempool.space first, falls back to 1ml.com. * Returns null if neither source has the node or both fail. */ -class NodeAliasService { +class NodeAliasClient { private val client = OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) @@ -34,15 +34,15 @@ class NodeAliasService { suspend fun resolveAlias(pubkey: String): String? { if (pubkey.isBlank()) return null - // 1. Try 1ml.com - runCatching { oneMlApi.getNode(pubkey) } + // 1. Try mempool.space + runCatching { mempoolApi.getNode(pubkey) } .onSuccess { response -> val alias = response.alias?.takeIf { it.isNotBlank() } if (alias != null) return alias } - // 2. Fall back to mempool.space - runCatching { mempoolApi.getNode(pubkey) } + // 2. Fall back to 1ml.com + runCatching { oneMlApi.getNode(pubkey) } .onSuccess { response -> val alias = response.alias?.takeIf { it.isNotBlank() } if (alias != null) return alias diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/LNbitsNodeAliasRepository.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/LNbitsNodeAliasRepository.kt index 19724e29..0627f89 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/LNbitsNodeAliasRepository.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/LNbitsNodeAliasRepository.kt @@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.StateFlow class LNbitsNodeAliasRepository( private val service: NodeAliasService ) : NodeAliasRepository { - override val aliases: StateFlow> get() = service.aliases - override suspend fun resolve(pubkey: String): String? = service.resolve(pubkey) + override suspend fun warmUp() = service.warmUp() + } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/LNbitsWalletRepository.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/LNbitsWalletRepository.kt index 4374fe7..ec2dbc1 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/LNbitsWalletRepository.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/LNbitsWalletRepository.kt @@ -1,10 +1,8 @@ package com.bitcointxoko.gudariwallet.data -import android.content.Context import android.net.Uri import android.util.Log import com.bitcointxoko.gudariwallet.api.* -import com.bitcointxoko.gudariwallet.api.LNbitsClient.httpClient import com.bitcointxoko.gudariwallet.security.SecretStore import com.bitcointxoko.gudariwallet.util.LnurlBech32 import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser @@ -15,9 +13,6 @@ private const val TAG = "LNbitsWalletRepo" class LNbitsWalletRepository( private val api: LNbitsApi, private val secrets: SecretStore, - private val context : Context, - private val nodeAliasService: com.bitcointxoko.gudariwallet.service.NodeAliasService = - com.bitcointxoko.gudariwallet.service.NodeAliasService(httpClient, context), ) : WalletRepository { // ── Balance ─────────────────────────────────────────────────────────────── @@ -157,7 +152,7 @@ class LNbitsWalletRepository( append(amountMsat) if (!comment.isNullOrBlank()) { append("&comment=") - append(android.net.Uri.encode(comment)) + append(Uri.encode(comment)) } } Log.d(TAG, "LNURL [FETCH INVOICE] callback=$callbackUrl") diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/NodeAliasCacheRepository.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/NodeAliasCacheRepository.kt new file mode 100644 index 0000000..1bff074 --- /dev/null +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/NodeAliasCacheRepository.kt @@ -0,0 +1,68 @@ +package com.bitcointxoko.gudariwallet.data + +import android.content.Context +import android.util.Log +import com.bitcointxoko.gudariwallet.data.db.AppDatabase +import com.bitcointxoko.gudariwallet.data.db.NodeAliasCacheEntity +import com.bitcointxoko.gudariwallet.util.WalletConstants + +private const val TAG = "NodeAliasCacheRepository" + +class NodeAliasCacheRepository(context: Context) { + + private val dao = AppDatabase.getInstance(context).nodeAliasCacheDao() + + /** + * Returns a cached alias for [pubkey] if it exists and is within + * [WalletConstants.ALIAS_CACHE_TTL_MS]. Returns null on miss or stale. + */ + suspend fun get(pubkey: String): String? { + val entry = dao.getByPubkey(pubkey) ?: run { + Log.d(TAG, "ALIAS [CACHE MISS ] $pubkey") + return null + } + val ageMs = System.currentTimeMillis() - entry.savedAt + return if (ageMs < WalletConstants.ALIAS_CACHE_TTL_MS) { + Log.d(TAG, "ALIAS [CACHE HIT ] $pubkey → \"${entry.alias}\" (age ${ageMs / 1000}s)") + entry.alias + } else { + Log.d(TAG, "ALIAS [CACHE STALE] $pubkey (age ${ageMs / 1000}s) — will refetch") + null + } + } + + /** Upsert an alias into the Room cache. */ + suspend fun put(pubkey: String, alias: String) { + dao.insert( + NodeAliasCacheEntity( + pubkey = pubkey, + alias = alias, + savedAt = System.currentTimeMillis() + ) + ) + Log.d(TAG, "ALIAS [PERSIST ] $pubkey → \"$alias\"") + } + + /** + * Load all non-stale entries from Room as a plain map. + * Used to seed the StateFlow on startup — replaces [loadPersistedCache]. + */ + suspend fun loadAll(): Map { + val now = System.currentTimeMillis() + return dao.getAll() + .filter { now - it.savedAt < WalletConstants.ALIAS_PERSIST_TTL_MS } + .associate { it.pubkey to it.alias } + .also { Log.d(TAG, "ALIAS [PERSIST ] Loaded ${it.size} non-stale entries from Room") } + } + + /** Remove a single entry. */ + suspend fun remove(pubkey: String) { + dao.deleteByPubkey(pubkey) + } + + /** Wipe the entire alias cache — mirrors [clearPersistedCache]. */ + suspend fun clearAll() { + dao.deleteAll() + Log.d(TAG, "ALIAS [PERSIST ] Cache cleared") + } +} diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/NodeAliasRepository.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/NodeAliasRepository.kt index a4f8818..786175f 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/NodeAliasRepository.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/NodeAliasRepository.kt @@ -18,4 +18,5 @@ interface NodeAliasRepository { * Results are cached — repeated calls for the same pubkey are cheap. */ suspend fun resolve(pubkey: String): String? + suspend fun warmUp() } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt index ed86a07..74b01ca 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/AppDatabase.kt @@ -9,8 +9,12 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase @Database( - entities = [PaymentRecordEntity::class, LnurlCacheEntity::class], - version = 2, + entities = [ + PaymentRecordEntity::class, + LnurlCacheEntity::class, + NodeAliasCacheEntity::class, // ← added + ], + version = 3, // ← bumped exportSchema = false ) @TypeConverters(PaymentConverters::class) @@ -18,6 +22,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun paymentDao(): PaymentDao abstract fun lnurlCacheDao(): LnurlCacheDao + abstract fun nodeAliasCacheDao(): NodeAliasCacheDao // ← added companion object { @Volatile private var INSTANCE: AppDatabase? = null @@ -37,6 +42,21 @@ abstract class AppDatabase : RoomDatabase() { } } + private val MIGRATION_2_3 = object : Migration(2, 3) { // ← added + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS `node_alias_cache` ( + `pubkey` TEXT NOT NULL, + `alias` TEXT NOT NULL, + `savedAt` INTEGER NOT NULL, + PRIMARY KEY(`pubkey`) + ) + """.trimIndent() + ) + } + } + fun getInstance(context: Context): AppDatabase = INSTANCE ?: synchronized(this) { INSTANCE ?: Room.databaseBuilder( @@ -44,7 +64,7 @@ abstract class AppDatabase : RoomDatabase() { AppDatabase::class.java, "gudari_wallet.db" ) - .addMigrations(MIGRATION_1_2) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3) // ← added .build() .also { INSTANCE = it } } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/NodeAliasCacheDao.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/NodeAliasCacheDao.kt new file mode 100644 index 0000000..430f19f --- /dev/null +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/NodeAliasCacheDao.kt @@ -0,0 +1,25 @@ +package com.bitcointxoko.gudariwallet.data.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface NodeAliasCacheDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entry: NodeAliasCacheEntity) + + @Query("SELECT * FROM node_alias_cache WHERE pubkey = :pubkey LIMIT 1") + suspend fun getByPubkey(pubkey: String): NodeAliasCacheEntity? + + @Query("SELECT * FROM node_alias_cache") + suspend fun getAll(): List + + @Query("DELETE FROM node_alias_cache WHERE pubkey = :pubkey") + suspend fun deleteByPubkey(pubkey: String) + + @Query("DELETE FROM node_alias_cache") + suspend fun deleteAll() +} diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/NodeAliasCacheEntity.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/NodeAliasCacheEntity.kt new file mode 100644 index 0000000..b37a874 --- /dev/null +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/NodeAliasCacheEntity.kt @@ -0,0 +1,12 @@ +package com.bitcointxoko.gudariwallet.data.db + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "node_alias_cache") +data class NodeAliasCacheEntity( + @PrimaryKey + val pubkey: String, // node public key + val alias: String, // resolved human-readable alias + val savedAt: Long // epoch ms — used for TTL checks +) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/service/NodeAliasService.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/service/NodeAliasService.kt index 181d815..90cfbcd 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/service/NodeAliasService.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/service/NodeAliasService.kt @@ -1,184 +1,54 @@ package com.bitcointxoko.gudariwallet.service import android.content.Context -import android.content.SharedPreferences import android.util.Log -import com.bitcointxoko.gudariwallet.util.WalletConstants -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.Dispatchers +import com.bitcointxoko.gudariwallet.api.NodeAliasClient as NodeAliasApiClient +import com.bitcointxoko.gudariwallet.data.NodeAliasCacheRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient -import okhttp3.Request -import java.util.concurrent.ConcurrentHashMap private const val TAG = "NodeAliasService" class NodeAliasService( - private val httpClient : OkHttpClient, - private val context : Context + private val apiClient: NodeAliasApiClient, // ← injected, not duplicated + context: Context ) { - - // ── Persistence ─────────────────────────────────────────────────────────── - - private val prefs: SharedPreferences = context.getSharedPreferences( - WalletConstants.ALIAS_PREFS_NAME, Context.MODE_PRIVATE - ) - private val gson = Gson() - - // ── In-memory alias cache ───────────────────────────────────────────────── - - private data class CachedAlias(val alias: String, val fetchedAt: Long) - - // Type token for Gson deserialisation - private val cacheType = object : TypeToken>() {}.type - - private val cache = ConcurrentHashMap() - - // ── Shared alias StateFlow — all screens collect this ───────────────────── + private val repo = NodeAliasCacheRepository(context) private val _aliases = MutableStateFlow>(emptyMap()) val aliases: StateFlow> = _aliases.asStateFlow() - // ── Init — load persisted cache from SharedPreferences ──────────────────── - - init { - loadPersistedCache() + suspend fun warmUp() { + _aliases.value = repo.loadAll() } - private fun loadPersistedCache() { - val json = prefs.getString(WalletConstants.ALIAS_PREFS_KEY, null) ?: run { - Log.d(TAG, "ALIAS [PERSIST ] No persisted cache found — starting fresh") - return - } - - runCatching { - val persisted: Map = gson.fromJson(json, cacheType) - val now = System.currentTimeMillis() - var loaded = 0 - var dropped = 0 - - persisted.forEach { (pubkey, entry) -> - val ageMs = now - entry.fetchedAt - if (ageMs < WalletConstants.ALIAS_PERSIST_TTL_MS) { - cache[pubkey] = entry - loaded++ - } else { - dropped++ - Log.d(TAG, "ALIAS [PERSIST ] Dropped stale entry for $pubkey (age ${ageMs / 3_600_000}h)") - } - } - - // Seed the StateFlow with everything we loaded - _aliases.value = cache.mapValues { it.value.alias } - - Log.d(TAG, "ALIAS [PERSIST ] Loaded $loaded entries, dropped $dropped stale entries") - }.onFailure { e -> - Log.w(TAG, "ALIAS [PERSIST ] Failed to deserialise cache — starting fresh: ${e.message}") - } - } - - private fun persistCache() { - runCatching { - val json = gson.toJson(cache) - prefs.edit().putString(WalletConstants.ALIAS_PREFS_KEY, json).apply() - Log.d(TAG, "ALIAS [PERSIST ] Wrote ${cache.size} entries to SharedPreferences") - }.onFailure { e -> - Log.w(TAG, "ALIAS [PERSIST ] Failed to write cache: ${e.message}") - } - } - - // ── Public API ──────────────────────────────────────────────────────────── - suspend fun resolve(pubkey: String): String? { - val now = System.currentTimeMillis() + // 1. Room cache hit + repo.get(pubkey)?.let { + Log.d(TAG, "ALIAS [CACHE HIT ] $pubkey → \"$it\"") + return it + } - cache[pubkey]?.let { entry -> - if (now - entry.fetchedAt < WalletConstants.ALIAS_CACHE_TTL_MS) { - Log.d(TAG, "ALIAS [CACHE HIT ] $pubkey → \"${entry.alias}\" (age ${(now - entry.fetchedAt) / 1000}s)") - return entry.alias - } else { - Log.d(TAG, "ALIAS [CACHE STALE] $pubkey (age ${(now - entry.fetchedAt) / 1000}s) — refetching") - } - } ?: Log.d(TAG, "ALIAS [CACHE MISS ] $pubkey — fetching from network") - - val alias = fetchFromNetwork(pubkey) - - if (alias == null) { - Log.w(TAG, "ALIAS [NOT FOUND ] $pubkey — no alias from mempool or 1ml") + // 2. Network via api/NodeAliasService (Retrofit, typed responses) + Log.d(TAG, "ALIAS [CACHE MISS] $pubkey — fetching from network") + val alias = apiClient.resolveAlias(pubkey) ?: run { + Log.w(TAG, "ALIAS [NOT FOUND ] $pubkey") return null } - // Update in-memory cache - cache[pubkey] = CachedAlias(alias = alias, fetchedAt = System.currentTimeMillis()) + // 3. Persist to Room + repo.put(pubkey, alias) - // Publish to shared StateFlow + // 4. Publish to StateFlow _aliases.value = _aliases.value + (pubkey to alias) - // Persist to SharedPreferences - persistCache() - - Log.d(TAG, "ALIAS [API FETCHED] $pubkey → \"$alias\"") + Log.d(TAG, "ALIAS [FETCHED ] $pubkey → \"$alias\"") return alias } - /** Clears in-memory cache, StateFlow, and persisted SharedPreferences entry. */ - fun clearPersistedCache() { - cache.clear() + suspend fun clearPersistedCache() { + repo.clearAll() _aliases.value = emptyMap() - prefs.edit().remove(WalletConstants.ALIAS_PREFS_KEY).apply() - Log.d(TAG, "ALIAS [PERSIST ] Cache cleared") } - - // ── Private helpers ─────────────────────────────────────────────────────── - - private suspend fun fetchFromNetwork(pubkey: String): String? = withContext(Dispatchers.IO) { - Log.d(TAG, "ALIAS [TRYING ] mempool.space for $pubkey") - val mempoolResult = fetchFromMempool(pubkey) - if (mempoolResult != null) { - Log.d(TAG, "ALIAS [SOURCE ] mempool.space resolved $pubkey → \"$mempoolResult\"") - return@withContext mempoolResult - } - Log.d(TAG, "ALIAS [TRYING ] 1ml.com fallback for $pubkey") - val mlResult = fetchFrom1ml(pubkey) - if (mlResult != null) { - Log.d(TAG, "ALIAS [SOURCE ] 1ml.com resolved $pubkey → \"$mlResult\"") - } - mlResult - } - - private fun fetchFromMempool(pubkey: String): String? { - return try { - val request = Request.Builder() - .url("https://mempool.space/api/v1/lightning/nodes/$pubkey") - .build() - httpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) return null - val body = response.body?.string() ?: return null - Regex(""""alias"\s*:\s*"([^"]+)"""").find(body)?.groupValues?.get(1) - } - } catch (e: Exception) { - Log.w(TAG, "mempool.space lookup failed for $pubkey: ${e.message}") - null - } - } - - private fun fetchFrom1ml(pubkey: String): String? { - return try { - val request = Request.Builder() - .url("https://1ml.com/node/$pubkey/json") - .build() - httpClient.newCall(request).execute().use { response -> - if (!response.isSuccessful) return null - val body = response.body?.string() ?: return null - Regex(""""alias"\s*:\s*"([^"]+)"""").find(body)?.groupValues?.get(1) - } - } catch (e: Exception) { - Log.w(TAG, "1ml.com lookup failed for $pubkey: ${e.message}") - null - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt index 206c5e8..195e7b3 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt @@ -67,6 +67,7 @@ class WalletViewModel( init { observePaymentEvents() fetchLightningAddress() + viewModelScope.launch { aliasRepo.warmUp() } } // ── WebSocket event collection ──────────────────────────────────────────── diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt index 8d8d592..b7e6288 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModelFactory.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.bitcointxoko.gudariwallet.api.LNbitsClient +import com.bitcointxoko.gudariwallet.api.NodeAliasClient import com.bitcointxoko.gudariwallet.data.BalancePrefsStore import com.bitcointxoko.gudariwallet.data.LNbitsNodeAliasRepository import com.bitcointxoko.gudariwallet.data.LNbitsWalletRepository @@ -24,12 +25,12 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F val secrets = EncryptedSecretStore(app) val baseUrl = secrets.baseUrl.ifBlank { "https://placeholder.invalid" } val api = LNbitsClient.create(baseUrl) - val nodeAliasSvc = NodeAliasService(httpClient = LNbitsClient.httpClient, context = app) + val nodeAliasSvc = NodeAliasService(apiClient = NodeAliasClient(), context = app) val lnurlCache = LnurlCacheRepository(app) val paymentCache = PaymentCacheRepository(app) val aliasRepo = LNbitsNodeAliasRepository(nodeAliasSvc) - val repo = LNbitsWalletRepository(api = api, secrets = secrets, context = app) + val repo = LNbitsWalletRepository(api = api, secrets = secrets) val balancePrefs = BalancePrefsStore(app) val balanceVm = BalanceViewModel(repo, balancePrefs) val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/util/WalletConstants.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/util/WalletConstants.kt index efde154..7f999c9 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/util/WalletConstants.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/util/WalletConstants.kt @@ -5,10 +5,6 @@ object WalletConstants { // ── Unit conversion ─────────────────────────────────────────────────────── const val MSAT_PER_SAT = 1_000L // millisatoshis per satoshi - // ── Polling ─────────────────────────────────────────────────────────────── - const val POLL_INTERVAL_MS = 3_000L // delay between payment status checks - const val POLL_MAX_ATTEMPTS = 200 // 200 × 3s = 10 minutes max - // ── WebSocket reconnect ─────────────────────────────────────────────────── const val WS_INITIAL_BACKOFF_MS = 1_000L // first retry delay const val WS_MAX_BACKOFF_MS = 16_000L // cap — never wait longer than this @@ -33,9 +29,7 @@ object WalletConstants { const val PAYMENTS_PAGE_SIZE = 20 // ── Alias cache ─────────────────────────────────────────────────────────── const val ALIAS_CACHE_TTL_MS = 3_600_000L // 1 hour — in-memory stale threshold - const val ALIAS_PREFS_NAME = "alias_cache_prefs" - const val ALIAS_PREFS_KEY = "alias_cache" - const val ALIAS_PERSIST_TTL_MS = 86_400_000L // 24 hours — drop on load if older than this + const val ALIAS_PERSIST_TTL_MS = 21 * 24 * 60 * 60 * 1000L // 21 days — drop on load if older than this // ── LNURL metadata cache ────────────────────────────────────────────────── const val LNURL_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000L // 7 days // ── Payments cache ──────────────────────────────────────────────────────── @@ -48,5 +42,4 @@ object WalletConstants { const val BALANCE_PREFS_KEY = "last_balance_sats" const val BALANCE_PREFS_SAVED_AT = "last_balance_saved_at" const val BALANCE_CACHE_TTL_MS = 3_600_000L // 1 hour - } \ No newline at end of file