refactor: extract LnurlScanner
This commit is contained in:
@@ -19,6 +19,7 @@ import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel
|
|||||||
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel
|
import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
|
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.send.LnurlScanner
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
||||||
|
|
||||||
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
|
||||||
@@ -26,11 +27,12 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
val secrets = EncryptedSecretStore(app)
|
val secrets = EncryptedSecretStore(app)
|
||||||
val api = LNbitsClient.create(secrets)
|
val api = LNbitsClient.create(secrets)
|
||||||
val nodeAliasSvc = NodeAliasService(apiClient = NodeAliasClient(), context = app)
|
val nodeAliasSvc = NodeAliasService(apiClient = NodeAliasClient(), context = app)
|
||||||
val lnurlCache = LnurlCacheRepository(app)
|
|
||||||
val paymentCache = PaymentCacheRepository(app)
|
|
||||||
val fiatDataStore = FiatDataStore(app)
|
|
||||||
val aliasRepo = NodeAliasServiceRepository(nodeAliasSvc)
|
val aliasRepo = NodeAliasServiceRepository(nodeAliasSvc)
|
||||||
val repo = LNbitsWalletRepository(api = api, secrets = secrets)
|
val repo = LNbitsWalletRepository(api = api, secrets = secrets)
|
||||||
|
val lnurlCache = LnurlCacheRepository(app)
|
||||||
|
val lnurlScanner = LnurlScanner(repo = repo, lnurlCache = lnurlCache)
|
||||||
|
val paymentCache = PaymentCacheRepository(app)
|
||||||
|
val fiatDataStore = FiatDataStore(app)
|
||||||
val balancePrefs = BalancePrefsStore(app)
|
val balancePrefs = BalancePrefsStore(app)
|
||||||
val lnAddressStore = LightningAddressStore(app)
|
val lnAddressStore = LightningAddressStore(app)
|
||||||
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
val balanceVm = BalanceViewModel(repo, balancePrefs)
|
||||||
@@ -43,7 +45,8 @@ class WalletViewModelFactory(private val app: Application) : ViewModelProvider.F
|
|||||||
balanceVm = balanceVm,
|
balanceVm = balanceVm,
|
||||||
onWithdrawScanned = { raw, lnurl ->
|
onWithdrawScanned = { raw, lnurl ->
|
||||||
receiveVm.handleWithdrawResponse(raw, lnurl)
|
receiveVm.handleWithdrawResponse(raw, lnurl)
|
||||||
}
|
},
|
||||||
|
scanner = lnurlScanner
|
||||||
)
|
)
|
||||||
|
|
||||||
val clipboardVm = ClipboardViewModel(
|
val clipboardVm = ClipboardViewModel(
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.send
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
|
||||||
|
import com.bitcointxoko.gudariwallet.data.LnurlCacheRepository
|
||||||
|
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||||
|
|
||||||
|
class LnurlScanner(
|
||||||
|
private val repo : WalletRepository,
|
||||||
|
private val lnurlCache : LnurlCacheRepository,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Cache → direct → proxy. Returns the raw response, or a failure with
|
||||||
|
* a descriptive exception. Does NOT write to cache on success — caller
|
||||||
|
* decides whether to cache (scan does, rescan does after validation).
|
||||||
|
*/
|
||||||
|
suspend fun scan(lnurl: String): Result<LnurlScanResult> {
|
||||||
|
// 1. Cache hit
|
||||||
|
val cached = lnurlCache.get(lnurl)
|
||||||
|
if (cached != null) {
|
||||||
|
Log.d(TAG, "LNURL [SCAN CACHE ] $lnurl")
|
||||||
|
return Result.success(LnurlScanResult(cached, fromCache = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Direct
|
||||||
|
val direct = runCatching { repo.scanLnurlDirect(lnurl) }
|
||||||
|
if (direct.isSuccess) {
|
||||||
|
Log.d(TAG, "LNURL [SCAN DIRECT] tag=${direct.getOrThrow().tag}")
|
||||||
|
return Result.success(LnurlScanResult(direct.getOrThrow().rawRes, fromCache = false))
|
||||||
|
}
|
||||||
|
Log.w(TAG, "LNURL [SCAN DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy")
|
||||||
|
|
||||||
|
// 3. Proxy fallback
|
||||||
|
val proxy = runCatching { repo.scanLnurl(lnurl) }
|
||||||
|
if (proxy.isSuccess) {
|
||||||
|
Log.d(TAG, "LNURL [SCAN PROXY ] tag=${proxy.getOrThrow().tag}")
|
||||||
|
return Result.success(LnurlScanResult(proxy.getOrThrow().rawRes, fromCache = false))
|
||||||
|
}
|
||||||
|
Log.e(TAG, "LNURL [SCAN PROXY FAIL] ${proxy.exceptionOrNull()?.message}")
|
||||||
|
return Result.failure(proxy.exceptionOrNull() ?: Exception("Scan failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same as [scan] but always skips the cache (used after cache invalidation). */
|
||||||
|
suspend fun rescan(lnurl: String): Result<LnurlScanResponse> {
|
||||||
|
val direct = runCatching { repo.scanLnurlDirect(lnurl) }
|
||||||
|
if (direct.isSuccess) {
|
||||||
|
Log.d(TAG, "LNURL [RESCAN DIRECT] success")
|
||||||
|
return Result.success(direct.getOrThrow().rawRes)
|
||||||
|
}
|
||||||
|
Log.w(TAG, "LNURL [RESCAN DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy")
|
||||||
|
|
||||||
|
val proxy = runCatching { repo.scanLnurl(lnurl) }
|
||||||
|
if (proxy.isSuccess) return Result.success(proxy.getOrThrow().rawRes)
|
||||||
|
|
||||||
|
Log.e(TAG, "LNURL [RESCAN ERR] ${proxy.exceptionOrNull()?.message}")
|
||||||
|
return Result.failure(proxy.exceptionOrNull() ?: Exception("Rescan failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LnurlScanResult(val raw: LnurlScanResponse, val fromCache: Boolean)
|
||||||
|
|
||||||
|
companion object { private const val TAG = "LnurlScanner" }
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ class SendViewModel(
|
|||||||
private val lnurlCache : LnurlCacheRepository,
|
private val lnurlCache : LnurlCacheRepository,
|
||||||
private val balanceVm : BalanceViewModel,
|
private val balanceVm : BalanceViewModel,
|
||||||
private val onWithdrawScanned: (raw: LnurlScanResponse, lnurl: String) -> Unit,
|
private val onWithdrawScanned: (raw: LnurlScanResponse, lnurl: String) -> Unit,
|
||||||
|
private val scanner : LnurlScanner,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
private val _sendState = MutableStateFlow<SendState>(SendState.Idle)
|
||||||
@@ -214,7 +215,7 @@ class SendViewModel(
|
|||||||
// ── 1. Initial attempt (direct → proxy)
|
// ── 1. Initial attempt (direct → proxy)
|
||||||
val initial = tryPayLnurlWithFallback(rawRes, lnurl, amountMsat, comment, "INITIAL", strings)
|
val initial = tryPayLnurlWithFallback(rawRes, lnurl, amountMsat, comment, "INITIAL", strings)
|
||||||
if (initial.isSuccess) {
|
if (initial.isSuccess) {
|
||||||
recordPaymentSuccess(initial.getOrThrow(), amountMsat)
|
recordPaymentSuccess(initial.getOrThrow(), amountMsat, strings = strings)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +246,7 @@ class SendViewModel(
|
|||||||
// ── 4. Final attempt (direct → proxy) with fresh metadata
|
// ── 4. Final attempt (direct → proxy) with fresh metadata
|
||||||
val retry = tryPayLnurlWithFallback(freshRaw, lnurl, amountMsat, comment, "RETRY", strings)
|
val retry = tryPayLnurlWithFallback(freshRaw, lnurl, amountMsat, comment, "RETRY", strings)
|
||||||
if (retry.isSuccess) {
|
if (retry.isSuccess) {
|
||||||
recordPaymentSuccess(retry.getOrThrow(), amountMsat)
|
recordPaymentSuccess(retry.getOrThrow(), amountMsat, strings = strings)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,24 +342,15 @@ class SendViewModel(
|
|||||||
|
|
||||||
if (directResult.isSuccess) {
|
if (directResult.isSuccess) {
|
||||||
val paymentHash = directResult.getOrThrow().paymentHash
|
val paymentHash = directResult.getOrThrow().paymentHash
|
||||||
val feeSats = runCatching {
|
handlePaymentResult(
|
||||||
repo.getPaymentDetail(paymentHash).details?.feeSat
|
paymentHash = paymentHash,
|
||||||
}.getOrNull()
|
amountSats = state.amountSats,
|
||||||
_sendState.value = SendState.PaymentSent(
|
bolt11 = state.bolt11,
|
||||||
amountSats = state.amountSats,
|
memo = state.memo,
|
||||||
feeSats = feeSats,
|
pubkey = state.payee,
|
||||||
paymentHash = paymentHash,
|
alias = state.nodeAlias,
|
||||||
pendingRecord = buildPendingRecord(
|
strings = strings,
|
||||||
paymentHash = paymentHash,
|
|
||||||
amountSats = state.amountSats,
|
|
||||||
feeSats = feeSats,
|
|
||||||
bolt11 = state.bolt11,
|
|
||||||
memo = state.memo,
|
|
||||||
pubkey = state.payee,
|
|
||||||
alias = state.nodeAlias
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
balanceVm.refreshBalance()
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +367,7 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Biometric gate. Shows prompt if device supports it; calls [pay] directly if not.
|
* Biometric gate. Shows prompt if device supports it; calls [pay] directly if not.
|
||||||
*/
|
*/
|
||||||
@@ -504,72 +497,33 @@ class SendViewModel(
|
|||||||
private fun handleLnurlScan(normalized: String, strings: SendStrings) {
|
private fun handleLnurlScan(normalized: String, strings: SendStrings) {
|
||||||
_sendState.value = SendState.Scanning
|
_sendState.value = SendState.Scanning
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// 1. Cache hit
|
val result = scanner.scan(normalized)
|
||||||
val cached = lnurlCache.get(normalized)
|
if (result.isFailure) {
|
||||||
if (cached != null) {
|
_sendState.value = SendState.Error(
|
||||||
Log.d(TAG, "LNURL [SCAN CACHE ] $normalized — skipping network call")
|
parseApiError(
|
||||||
when (cached.tag) {
|
result.exceptionOrNull() ?: Exception("Scan failed"),
|
||||||
"withdrawRequest" -> {
|
strings.couldNotResolveAddress
|
||||||
_sendState.value = SendState.Idle
|
) )
|
||||||
onWithdrawScanned(cached, normalized)
|
|
||||||
}
|
|
||||||
else -> _sendState.value = buildLnurlReadyState(cached, normalized)
|
|
||||||
}
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
Log.d(TAG, "LNURL [SCAN START ] normalized=$normalized")
|
|
||||||
|
|
||||||
// 2. Client-side direct
|
val (raw, fromCache) = result.getOrThrow()
|
||||||
val directResult = runCatching { repo.scanLnurlDirect(normalized) }
|
|
||||||
if (directResult.isSuccess) {
|
val protocolError = lnurlProtocolError(raw)
|
||||||
val scanned = directResult.getOrThrow()
|
if (protocolError != null) {
|
||||||
Log.d(TAG, "LNURL [SCAN DIRECT] tag=${scanned.tag} for $normalized")
|
Log.w(TAG, "LNURL [SCAN PROTO ERR] $protocolError")
|
||||||
val protocolError = lnurlProtocolError(scanned.rawRes)
|
_sendState.value = SendState.Error(protocolError)
|
||||||
if (protocolError != null) {
|
|
||||||
Log.w(TAG, "LNURL [SCAN PROTO ERR] $protocolError")
|
|
||||||
_sendState.value = SendState.Error(protocolError)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
lnurlCache.put(normalized, scanned.rawRes)
|
|
||||||
when (scanned.tag) {
|
|
||||||
"payRequest" -> _sendState.value = buildLnurlReadyState(scanned.rawRes, normalized)
|
|
||||||
"withdrawRequest" -> {
|
|
||||||
_sendState.value = SendState.Idle
|
|
||||||
onWithdrawScanned(scanned.rawRes, normalized)
|
|
||||||
}
|
|
||||||
null -> _sendState.value = SendState.Error(strings.emptyResponse) // ← "Empty response from server"
|
|
||||||
else -> _sendState.value = SendState.Error(strings.unsupportedType(scanned.tag)) // ← "Unsupported type: $tag"
|
|
||||||
}
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
Log.w(TAG, "LNURL [SCAN DIRECT FAIL] ${directResult.exceptionOrNull()?.message} — falling back to proxy")
|
|
||||||
|
|
||||||
// 3. Server proxy fallback
|
if (!fromCache) lnurlCache.put(normalized, raw)
|
||||||
runCatching { repo.scanLnurl(normalized) }
|
|
||||||
.onSuccess { scanned ->
|
when (raw.tag) {
|
||||||
Log.d(TAG, "LNURL [SCAN PROXY ] tag=${scanned.tag} for $normalized")
|
"payRequest" -> _sendState.value = buildLnurlReadyState(raw, normalized)
|
||||||
val protocolError = lnurlProtocolError(scanned.rawRes)
|
"withdrawRequest" -> { _sendState.value = SendState.Idle; onWithdrawScanned(raw, normalized) }
|
||||||
if (protocolError != null) {
|
null -> _sendState.value = SendState.Error(strings.emptyResponse)
|
||||||
Log.w(TAG, "LNURL [SCAN PROTO ERR] $protocolError")
|
else -> _sendState.value = SendState.Error(strings.unsupportedType(raw.tag))
|
||||||
_sendState.value = SendState.Error(protocolError)
|
}
|
||||||
return@onSuccess
|
|
||||||
}
|
|
||||||
lnurlCache.put(normalized, scanned.rawRes)
|
|
||||||
when (scanned.tag) {
|
|
||||||
"payRequest" -> _sendState.value = buildLnurlReadyState(scanned.rawRes, normalized)
|
|
||||||
"withdrawRequest" -> {
|
|
||||||
_sendState.value = SendState.Idle
|
|
||||||
onWithdrawScanned(scanned.rawRes, normalized)
|
|
||||||
}
|
|
||||||
null -> _sendState.value = SendState.Error(strings.emptyResponse) // ← "Empty response from server"
|
|
||||||
else -> _sendState.value = SendState.Error(strings.unsupportedType(scanned.tag)) // ← "Unsupported type: $tag"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFailure { e ->
|
|
||||||
_sendState.value = SendState.Error(
|
|
||||||
parseApiError(e, strings.couldNotResolveAddress) // ← "Could not resolve address"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,27 +548,22 @@ class SendViewModel(
|
|||||||
bolt11 : String? = null,
|
bolt11 : String? = null,
|
||||||
memo : String? = null,
|
memo : String? = null,
|
||||||
pubkey : String? = null,
|
pubkey : String? = null,
|
||||||
alias : String? = null
|
alias : String? = null,
|
||||||
|
strings : SendStrings,
|
||||||
) {
|
) {
|
||||||
val feeSats = runCatching { repo.getPaymentDetail(paymentHash).details?.feeSat }.getOrNull()
|
|
||||||
val amountSats = amountMsat / WalletConstants.MSAT_PER_SAT
|
val amountSats = amountMsat / WalletConstants.MSAT_PER_SAT
|
||||||
_sendState.value = SendState.PaymentSent(
|
handlePaymentResult(
|
||||||
amountSats = amountSats,
|
paymentHash = paymentHash,
|
||||||
feeSats = feeSats,
|
amountSats = amountSats,
|
||||||
paymentHash = paymentHash,
|
bolt11 = bolt11,
|
||||||
pendingRecord = buildPendingRecord(
|
memo = memo,
|
||||||
paymentHash = paymentHash,
|
pubkey = pubkey,
|
||||||
amountSats = amountSats,
|
alias = alias,
|
||||||
feeSats = feeSats,
|
strings = strings,
|
||||||
bolt11 = bolt11,
|
|
||||||
memo = memo,
|
|
||||||
pubkey = pubkey,
|
|
||||||
alias = alias
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
balanceVm.refreshBalance()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun tryPayLnurlWithFallback(
|
private suspend fun tryPayLnurlWithFallback(
|
||||||
rawRes : LnurlScanResponse,
|
rawRes : LnurlScanResponse,
|
||||||
lnurl : String,
|
lnurl : String,
|
||||||
@@ -642,21 +591,14 @@ class SendViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun rescanLnurl(lnurl: String, strings: SendStrings): LnurlScanResponse? {
|
private suspend fun rescanLnurl(lnurl: String, strings: SendStrings): LnurlScanResponse? {
|
||||||
val direct = runCatching { repo.scanLnurlDirect(lnurl) }
|
val result = scanner.rescan(lnurl)
|
||||||
if (direct.isSuccess) {
|
if (result.isSuccess) return result.getOrThrow()
|
||||||
Log.d(TAG, "LNURL [RESCAN DIRECT] success")
|
|
||||||
return direct.getOrThrow().rawRes
|
|
||||||
}
|
|
||||||
Log.w(TAG, "LNURL [RESCAN DIRECT FAIL] ${direct.exceptionOrNull()?.message} — trying proxy rescan")
|
|
||||||
|
|
||||||
val proxy = runCatching { repo.scanLnurl(lnurl) }
|
Log.e(TAG, "LNURL [RESCAN ERR] ${result.exceptionOrNull()?.message}")
|
||||||
if (proxy.isSuccess) return proxy.getOrThrow().rawRes
|
|
||||||
|
|
||||||
Log.e(TAG, "LNURL [RESCAN ERR] ${proxy.exceptionOrNull()?.message}")
|
|
||||||
_sendState.value = SendState.Error(
|
_sendState.value = SendState.Error(
|
||||||
parseApiError(
|
parseApiError(
|
||||||
proxy.exceptionOrNull() ?: Exception(),
|
result.exceptionOrNull() ?: Exception("Rescan failed"),
|
||||||
strings.paymentFailedRescan // ← "Payment failed — could not re-fetch address"
|
strings.paymentFailedRescan
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user