mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
feat(wallet): tappable onchain rows + txid index in LocalCache
- LocalCache now keeps a ConcurrentHashMap<txid, OnchainZapEvent> populated in consume(OnchainZapEvent). First sender to claim a txid wins. The on-chain transactions ViewModel switches to LocalCache.getOnchainZapByTxid(txid), replacing the per-row scan of notes. - Each row in OnchainTransactionsScreen is now clickable: rows with a matched zap event navigate to the zap's note thread; the rest open the transaction on mempool.space in the system browser.
This commit is contained in:
@@ -271,6 +271,7 @@ import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.SortedSet
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
interface ILocalCache {
|
||||
fun markAsSeen(
|
||||
@@ -295,6 +296,18 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
|
||||
val paymentTracker = NwcPaymentTracker()
|
||||
|
||||
/**
|
||||
* Index from on-chain transaction id → the first [OnchainZapEvent] we
|
||||
* accepted that claims it. Populated in [consume]`(OnchainZapEvent)` after
|
||||
* the event passes verification. The wallet's on-chain history view uses
|
||||
* it to attribute a Nostr sender/recipient to a chain row without scanning
|
||||
* [notes] for every transaction.
|
||||
*/
|
||||
private val onchainZapsByTxid = ConcurrentHashMap<String, OnchainZapEvent>()
|
||||
|
||||
/** Returns the cached [OnchainZapEvent] for [txid], if any. */
|
||||
fun getOnchainZapByTxid(txid: String): OnchainZapEvent? = onchainZapsByTxid[txid]
|
||||
|
||||
/**
|
||||
* Bitcoin chain backend used by [consume]`(OnchainZapEvent)` to verify NIP-BC zaps
|
||||
* against the actual on-chain transaction. `null` disables verification (incoming
|
||||
@@ -1761,6 +1774,11 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
refreshNewNoteObservers(note)
|
||||
|
||||
// First sender to claim a txid wins the index — duplicates are rare
|
||||
// (the verifier proves the tx pays the recipient) and keeping a single
|
||||
// attribution matches what the on-chain wallet UI shows.
|
||||
event.txid()?.let { onchainZapsByTxid.putIfAbsent(it, event) }
|
||||
|
||||
// Verification needs a chain backend. Without one (e.g. before Account
|
||||
// wires its EsploraBackend) the event is still cached so subscriptions
|
||||
// and profile zap views see it, but it can't contribute to Note totals.
|
||||
|
||||
+34
-1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -50,6 +51,8 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -59,6 +62,7 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -165,6 +169,7 @@ fun OnchainTransactionsScreen(
|
||||
EmptyMessage(padding, stringRes(R.string.wallet_no_transactions))
|
||||
}
|
||||
else -> {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
state = listState,
|
||||
@@ -174,7 +179,12 @@ fun OnchainTransactionsScreen(
|
||||
TransactionFilterRow(currentFilter) { viewModel.setTransactionFilter(it) }
|
||||
}
|
||||
items(transactions, key = { it.tx.txid }) { txView ->
|
||||
OnchainTransactionItem(txView, accountViewModel, nav)
|
||||
OnchainTransactionItem(
|
||||
view = txView,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onClick = { handleTxClick(txView, nav, uriHandler) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
if (isLoadingMore) {
|
||||
@@ -268,6 +278,7 @@ private fun OnchainTransactionItem(
|
||||
view: OnchainTxView,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val isIncoming = view.isIncoming
|
||||
val amountSats = view.tx.netValueSats.absoluteValue
|
||||
@@ -295,6 +306,7 @@ private fun OnchainTransactionItem(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@@ -390,6 +402,27 @@ private fun OnchainTransactionItem(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a transaction-row tap: jump to the matched on-chain zap event's
|
||||
* thread when we have one, otherwise open the tx on a public block explorer.
|
||||
* Mempool.space works over Tor and clearnet; deriving the user's configured
|
||||
* explorer URL would also need to know whether Bitcoin traffic is being
|
||||
* routed over Tor right now, which the UI layer doesn't carry — stick with
|
||||
* mempool.space as a sensible default.
|
||||
*/
|
||||
private fun handleTxClick(
|
||||
view: OnchainTxView,
|
||||
nav: INav,
|
||||
uriHandler: UriHandler,
|
||||
) {
|
||||
val zap = view.zap
|
||||
if (zap != null) {
|
||||
nav.nav(Route.Note(zap.id))
|
||||
} else {
|
||||
runCatching { uriHandler.openUri("https://mempool.space/tx/${view.tx.txid}") }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnchainCounterpartyName(
|
||||
pubkeyHex: String,
|
||||
|
||||
+2
-21
@@ -123,7 +123,7 @@ class OnchainTransactionsViewModel : ViewModel() {
|
||||
_hasMoreTransactions.value = true
|
||||
try {
|
||||
val rows = withContext(Dispatchers.IO) { be.getTxsForAddress(addr, null) }
|
||||
val views = rows.map { OnchainTxView(it, findZapForTxid(it.txid)) }
|
||||
val views = rows.map { OnchainTxView(it, LocalCache.getOnchainZapByTxid(it.txid)) }
|
||||
allTransactions.value = views
|
||||
lastSeenTxid = rows.lastOrNull { it.confirmations > 0 }?.txid
|
||||
_hasMoreTransactions.value = lastSeenTxid != null
|
||||
@@ -148,7 +148,7 @@ class OnchainTransactionsViewModel : ViewModel() {
|
||||
if (rows.isEmpty()) {
|
||||
_hasMoreTransactions.value = false
|
||||
} else {
|
||||
val views = rows.map { OnchainTxView(it, findZapForTxid(it.txid)) }
|
||||
val views = rows.map { OnchainTxView(it, LocalCache.getOnchainZapByTxid(it.txid)) }
|
||||
allTransactions.value = allTransactions.value + views
|
||||
lastSeenTxid = rows.lastOrNull { it.confirmations > 0 }?.txid ?: seen
|
||||
}
|
||||
@@ -163,23 +163,4 @@ class OnchainTransactionsViewModel : ViewModel() {
|
||||
fun clearError() {
|
||||
_error.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan `LocalCache.notes` for the [OnchainZapEvent] that references this
|
||||
* txid. NIP-BC is anti-spoofing-checked on consume, so any event we find
|
||||
* here has already been accepted as authentic and matches our chain row.
|
||||
* There's no txid-indexed map yet — the scan stays acceptable while
|
||||
* on-chain zap volume is small.
|
||||
*/
|
||||
private fun findZapForTxid(txid: String): OnchainZapEvent? {
|
||||
var found: OnchainZapEvent? = null
|
||||
LocalCache.notes.forEach { _, note ->
|
||||
if (found != null) return@forEach
|
||||
val ev = note.event
|
||||
if (ev is OnchainZapEvent && ev.txid() == txid) {
|
||||
found = ev
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user