feat: migrate nodeAliasCache to room
This commit is contained in:
+6
-6
@@ -7,10 +7,10 @@ import java.util.concurrent.TimeUnit
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a Lightning node pubkey to a human-readable alias.
|
* 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.
|
* Returns null if neither source has the node or both fail.
|
||||||
*/
|
*/
|
||||||
class NodeAliasService {
|
class NodeAliasClient {
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
private val client = OkHttpClient.Builder()
|
||||||
.connectTimeout(5, TimeUnit.SECONDS)
|
.connectTimeout(5, TimeUnit.SECONDS)
|
||||||
@@ -34,15 +34,15 @@ class NodeAliasService {
|
|||||||
suspend fun resolveAlias(pubkey: String): String? {
|
suspend fun resolveAlias(pubkey: String): String? {
|
||||||
if (pubkey.isBlank()) return null
|
if (pubkey.isBlank()) return null
|
||||||
|
|
||||||
// 1. Try 1ml.com
|
// 1. Try mempool.space
|
||||||
runCatching { oneMlApi.getNode(pubkey) }
|
runCatching { mempoolApi.getNode(pubkey) }
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
val alias = response.alias?.takeIf { it.isNotBlank() }
|
val alias = response.alias?.takeIf { it.isNotBlank() }
|
||||||
if (alias != null) return alias
|
if (alias != null) return alias
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fall back to mempool.space
|
// 2. Fall back to 1ml.com
|
||||||
runCatching { mempoolApi.getNode(pubkey) }
|
runCatching { oneMlApi.getNode(pubkey) }
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
val alias = response.alias?.takeIf { it.isNotBlank() }
|
val alias = response.alias?.takeIf { it.isNotBlank() }
|
||||||
if (alias != null) return alias
|
if (alias != null) return alias
|
||||||
@@ -10,10 +10,10 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
class LNbitsNodeAliasRepository(
|
class LNbitsNodeAliasRepository(
|
||||||
private val service: NodeAliasService
|
private val service: NodeAliasService
|
||||||
) : NodeAliasRepository {
|
) : NodeAliasRepository {
|
||||||
|
|
||||||
override val aliases: StateFlow<Map<String, String>>
|
override val aliases: StateFlow<Map<String, String>>
|
||||||
get() = service.aliases
|
get() = service.aliases
|
||||||
|
|
||||||
override suspend fun resolve(pubkey: String): String? =
|
override suspend fun resolve(pubkey: String): String? =
|
||||||
service.resolve(pubkey)
|
service.resolve(pubkey)
|
||||||
|
override suspend fun warmUp() = service.warmUp()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.bitcointxoko.gudariwallet.data
|
package com.bitcointxoko.gudariwallet.data
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitcointxoko.gudariwallet.api.*
|
import com.bitcointxoko.gudariwallet.api.*
|
||||||
import com.bitcointxoko.gudariwallet.api.LNbitsClient.httpClient
|
|
||||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||||
import com.bitcointxoko.gudariwallet.util.LnurlBech32
|
import com.bitcointxoko.gudariwallet.util.LnurlBech32
|
||||||
import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser
|
import com.bitcointxoko.gudariwallet.util.LnurlMetadataParser
|
||||||
@@ -15,9 +13,6 @@ private const val TAG = "LNbitsWalletRepo"
|
|||||||
class LNbitsWalletRepository(
|
class LNbitsWalletRepository(
|
||||||
private val api: LNbitsApi,
|
private val api: LNbitsApi,
|
||||||
private val secrets: SecretStore,
|
private val secrets: SecretStore,
|
||||||
private val context : Context,
|
|
||||||
private val nodeAliasService: com.bitcointxoko.gudariwallet.service.NodeAliasService =
|
|
||||||
com.bitcointxoko.gudariwallet.service.NodeAliasService(httpClient, context),
|
|
||||||
) : WalletRepository {
|
) : WalletRepository {
|
||||||
|
|
||||||
// ── Balance ───────────────────────────────────────────────────────────────
|
// ── Balance ───────────────────────────────────────────────────────────────
|
||||||
@@ -157,7 +152,7 @@ class LNbitsWalletRepository(
|
|||||||
append(amountMsat)
|
append(amountMsat)
|
||||||
if (!comment.isNullOrBlank()) {
|
if (!comment.isNullOrBlank()) {
|
||||||
append("&comment=")
|
append("&comment=")
|
||||||
append(android.net.Uri.encode(comment))
|
append(Uri.encode(comment))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "LNURL [FETCH INVOICE] callback=$callbackUrl")
|
Log.d(TAG, "LNURL [FETCH INVOICE] callback=$callbackUrl")
|
||||||
|
|||||||
@@ -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<String, String> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,4 +18,5 @@ interface NodeAliasRepository {
|
|||||||
* Results are cached — repeated calls for the same pubkey are cheap.
|
* Results are cached — repeated calls for the same pubkey are cheap.
|
||||||
*/
|
*/
|
||||||
suspend fun resolve(pubkey: String): String?
|
suspend fun resolve(pubkey: String): String?
|
||||||
|
suspend fun warmUp()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import androidx.room.migration.Migration
|
|||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [PaymentRecordEntity::class, LnurlCacheEntity::class],
|
entities = [
|
||||||
version = 2,
|
PaymentRecordEntity::class,
|
||||||
|
LnurlCacheEntity::class,
|
||||||
|
NodeAliasCacheEntity::class, // ← added
|
||||||
|
],
|
||||||
|
version = 3, // ← bumped
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(PaymentConverters::class)
|
@TypeConverters(PaymentConverters::class)
|
||||||
@@ -18,6 +22,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
abstract fun paymentDao(): PaymentDao
|
abstract fun paymentDao(): PaymentDao
|
||||||
abstract fun lnurlCacheDao(): LnurlCacheDao
|
abstract fun lnurlCacheDao(): LnurlCacheDao
|
||||||
|
abstract fun nodeAliasCacheDao(): NodeAliasCacheDao // ← added
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile private var INSTANCE: AppDatabase? = null
|
@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 =
|
fun getInstance(context: Context): AppDatabase =
|
||||||
INSTANCE ?: synchronized(this) {
|
INSTANCE ?: synchronized(this) {
|
||||||
INSTANCE ?: Room.databaseBuilder(
|
INSTANCE ?: Room.databaseBuilder(
|
||||||
@@ -44,7 +64,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
AppDatabase::class.java,
|
AppDatabase::class.java,
|
||||||
"gudari_wallet.db"
|
"gudari_wallet.db"
|
||||||
)
|
)
|
||||||
.addMigrations(MIGRATION_1_2)
|
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) // ← added
|
||||||
.build()
|
.build()
|
||||||
.also { INSTANCE = it }
|
.also { INSTANCE = it }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<NodeAliasCacheEntity>
|
||||||
|
|
||||||
|
@Query("DELETE FROM node_alias_cache WHERE pubkey = :pubkey")
|
||||||
|
suspend fun deleteByPubkey(pubkey: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM node_alias_cache")
|
||||||
|
suspend fun deleteAll()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -1,184 +1,54 @@
|
|||||||
package com.bitcointxoko.gudariwallet.service
|
package com.bitcointxoko.gudariwallet.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
import com.bitcointxoko.gudariwallet.api.NodeAliasClient as NodeAliasApiClient
|
||||||
import com.google.gson.Gson
|
import com.bitcointxoko.gudariwallet.data.NodeAliasCacheRepository
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
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"
|
private const val TAG = "NodeAliasService"
|
||||||
|
|
||||||
class NodeAliasService(
|
class NodeAliasService(
|
||||||
private val httpClient : OkHttpClient,
|
private val apiClient: NodeAliasApiClient, // ← injected, not duplicated
|
||||||
private val context : Context
|
context: Context
|
||||||
) {
|
) {
|
||||||
|
private val repo = NodeAliasCacheRepository(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<Map<String, CachedAlias>>() {}.type
|
|
||||||
|
|
||||||
private val cache = ConcurrentHashMap<String, CachedAlias>()
|
|
||||||
|
|
||||||
// ── Shared alias StateFlow — all screens collect this ─────────────────────
|
|
||||||
|
|
||||||
private val _aliases = MutableStateFlow<Map<String, String>>(emptyMap())
|
private val _aliases = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||||
val aliases: StateFlow<Map<String, String>> = _aliases.asStateFlow()
|
val aliases: StateFlow<Map<String, String>> = _aliases.asStateFlow()
|
||||||
|
|
||||||
// ── Init — load persisted cache from SharedPreferences ────────────────────
|
suspend fun warmUp() {
|
||||||
|
_aliases.value = repo.loadAll()
|
||||||
init {
|
|
||||||
loadPersistedCache()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<String, CachedAlias> = 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? {
|
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 ->
|
// 2. Network via api/NodeAliasService (Retrofit, typed responses)
|
||||||
if (now - entry.fetchedAt < WalletConstants.ALIAS_CACHE_TTL_MS) {
|
Log.d(TAG, "ALIAS [CACHE MISS] $pubkey — fetching from network")
|
||||||
Log.d(TAG, "ALIAS [CACHE HIT ] $pubkey → \"${entry.alias}\" (age ${(now - entry.fetchedAt) / 1000}s)")
|
val alias = apiClient.resolveAlias(pubkey) ?: run {
|
||||||
return entry.alias
|
Log.w(TAG, "ALIAS [NOT FOUND ] $pubkey")
|
||||||
} 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")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update in-memory cache
|
// 3. Persist to Room
|
||||||
cache[pubkey] = CachedAlias(alias = alias, fetchedAt = System.currentTimeMillis())
|
repo.put(pubkey, alias)
|
||||||
|
|
||||||
// Publish to shared StateFlow
|
// 4. Publish to StateFlow
|
||||||
_aliases.value = _aliases.value + (pubkey to alias)
|
_aliases.value = _aliases.value + (pubkey to alias)
|
||||||
|
|
||||||
// Persist to SharedPreferences
|
Log.d(TAG, "ALIAS [FETCHED ] $pubkey → \"$alias\"")
|
||||||
persistCache()
|
|
||||||
|
|
||||||
Log.d(TAG, "ALIAS [API FETCHED] $pubkey → \"$alias\"")
|
|
||||||
return alias
|
return alias
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clears in-memory cache, StateFlow, and persisted SharedPreferences entry. */
|
suspend fun clearPersistedCache() {
|
||||||
fun clearPersistedCache() {
|
repo.clearAll()
|
||||||
cache.clear()
|
|
||||||
_aliases.value = emptyMap()
|
_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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class WalletViewModel(
|
|||||||
init {
|
init {
|
||||||
observePaymentEvents()
|
observePaymentEvents()
|
||||||
fetchLightningAddress()
|
fetchLightningAddress()
|
||||||
|
viewModelScope.launch { aliasRepo.warmUp() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WebSocket event collection ────────────────────────────────────────────
|
// ── WebSocket event collection ────────────────────────────────────────────
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.bitcointxoko.gudariwallet.api.LNbitsClient
|
import com.bitcointxoko.gudariwallet.api.LNbitsClient
|
||||||
|
import com.bitcointxoko.gudariwallet.api.NodeAliasClient
|
||||||
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
import com.bitcointxoko.gudariwallet.data.BalancePrefsStore
|
||||||
import com.bitcointxoko.gudariwallet.data.LNbitsNodeAliasRepository
|
import com.bitcointxoko.gudariwallet.data.LNbitsNodeAliasRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.LNbitsWalletRepository
|
import com.bitcointxoko.gudariwallet.data.LNbitsWalletRepository
|
||||||
@@ -24,12 +25,12 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
val secrets = EncryptedSecretStore(app)
|
val secrets = EncryptedSecretStore(app)
|
||||||
val baseUrl = secrets.baseUrl.ifBlank { "https://placeholder.invalid" }
|
val baseUrl = secrets.baseUrl.ifBlank { "https://placeholder.invalid" }
|
||||||
val api = LNbitsClient.create(baseUrl)
|
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 lnurlCache = LnurlCacheRepository(app)
|
||||||
val paymentCache = PaymentCacheRepository(app)
|
val paymentCache = PaymentCacheRepository(app)
|
||||||
|
|
||||||
val aliasRepo = LNbitsNodeAliasRepository(nodeAliasSvc)
|
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 balancePrefs = BalancePrefsStore(app)
|
||||||
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
||||||
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm)
|
val fiatVm = FiatViewModel(repo, balancePrefs, balanceVm)
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ object WalletConstants {
|
|||||||
// ── Unit conversion ───────────────────────────────────────────────────────
|
// ── Unit conversion ───────────────────────────────────────────────────────
|
||||||
const val MSAT_PER_SAT = 1_000L // millisatoshis per satoshi
|
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 ───────────────────────────────────────────────────
|
// ── WebSocket reconnect ───────────────────────────────────────────────────
|
||||||
const val WS_INITIAL_BACKOFF_MS = 1_000L // first retry delay
|
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
|
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
|
const val PAYMENTS_PAGE_SIZE = 20
|
||||||
// ── Alias cache ───────────────────────────────────────────────────────────
|
// ── Alias cache ───────────────────────────────────────────────────────────
|
||||||
const val ALIAS_CACHE_TTL_MS = 3_600_000L // 1 hour — in-memory stale threshold
|
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_PERSIST_TTL_MS = 21 * 24 * 60 * 60 * 1000L // 21 days — drop on load if older than this
|
||||||
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
|
|
||||||
// ── LNURL metadata cache ──────────────────────────────────────────────────
|
// ── LNURL metadata cache ──────────────────────────────────────────────────
|
||||||
const val LNURL_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000L // 7 days
|
const val LNURL_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000L // 7 days
|
||||||
// ── Payments cache ────────────────────────────────────────────────────────
|
// ── Payments cache ────────────────────────────────────────────────────────
|
||||||
@@ -48,5 +42,4 @@ object WalletConstants {
|
|||||||
const val BALANCE_PREFS_KEY = "last_balance_sats"
|
const val BALANCE_PREFS_KEY = "last_balance_sats"
|
||||||
const val BALANCE_PREFS_SAVED_AT = "last_balance_saved_at"
|
const val BALANCE_PREFS_SAVED_AT = "last_balance_saved_at"
|
||||||
const val BALANCE_CACHE_TTL_MS = 3_600_000L // 1 hour
|
const val BALANCE_CACHE_TTL_MS = 3_600_000L // 1 hour
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user