mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
fix(onchain): highlight bolt for own onchain zaps + own pending in counter
Two parallel gaps to the cashu work, surfaced once the cashu side was wired correctly: 1. The orange bolt highlight on the reaction row never lit up for onchain zaps. Note.isZappedBy checked LN zaps, NWC payments, and (since Phase 1) nutzaps — but never onchainZaps. And the fast-path gate in ObserveZapIconState shared the same blind spot. Add isOnchainZappedBy parallel to isNutzappedBy (same shape: any onchainZaps entry whose source.author matches the user and whose source event is newer than afterTimeInSeconds), and extend the gate with onchainZaps?.isNotEmpty(). 2. The reaction-row counter included CONFIRMED onchain amounts via updateZapTotal (verifiedSats only, per NIP-BC) but not the signed-in user's OWN pending/unverified outgoing zaps. That created a UX mismatch: the gallery shows the user's own UNVERIFIED entry with its claimed sat amount immediately (the user knows what they sent), but the counter stays at 0 until the chain catches up. Add Note.extraOwnPendingOnchainSats(loggedInPubKey) that sums claimedSats from non-CONFIRMED onchainZaps whose source.author matches the logged-in pubkey, and add it on top of zapsAmount in both AccountViewModel.calculateZapAmount paths and ObserveZapAmountText's no-zapPayments fast path. Other senders' non-confirmed entries still contribute 0, preserving the anti-spoof posture for incoming zaps.
This commit is contained in:
@@ -1458,7 +1458,8 @@ fun ObserveZapIconState(
|
||||
val hasZapData =
|
||||
zapsState?.note?.zapPayments?.isNotEmpty() == true ||
|
||||
zapsState?.note?.zaps?.isNotEmpty() == true ||
|
||||
zapsState?.note?.nutzaps?.isNotEmpty() == true
|
||||
zapsState?.note?.nutzaps?.isNotEmpty() == true ||
|
||||
zapsState?.note?.onchainZaps?.isNotEmpty() == true
|
||||
val wasZapped =
|
||||
if (hasZapData) {
|
||||
accountViewModel.calculateIfNoteWasZappedByAccount(baseNote, afterTimeInSeconds)
|
||||
@@ -1538,7 +1539,14 @@ fun ObserveZapAmountText(
|
||||
|
||||
inner(zapAmountTxt)
|
||||
} else {
|
||||
inner(showAmount(zapsState?.note?.zapsAmount))
|
||||
// Include the signed-in user's own pending onchain zaps so
|
||||
// the counter reflects the optimistic value the gallery shows.
|
||||
val ownPubKey = accountViewModel.account.userProfile().pubkeyHex
|
||||
val note = zapsState?.note
|
||||
val total =
|
||||
(note?.zapsAmount ?: java.math.BigDecimal(0)) +
|
||||
java.math.BigDecimal(note?.extraOwnPendingOnchainSats(ownPubKey) ?: 0L)
|
||||
inner(showAmount(total))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-5
@@ -623,15 +623,21 @@ class AccountViewModel(
|
||||
account.calculateIfNoteWasZappedByAccount(zappedNote, afterTimeInSeconds)
|
||||
}
|
||||
|
||||
suspend fun calculateZapAmount(zappedNote: Note): String =
|
||||
if (zappedNote.zapPayments.isNotEmpty()) {
|
||||
suspend fun calculateZapAmount(zappedNote: Note): String {
|
||||
// The signed-in user's own outgoing onchain zaps that aren't
|
||||
// yet CONFIRMED still need to show in the counter — the user
|
||||
// knows what they sent, so make the counter reflect reality
|
||||
// immediately instead of waiting for chain confirmation.
|
||||
val ownPendingOnchain = zappedNote.extraOwnPendingOnchainSats(account.userProfile().pubkeyHex)
|
||||
return if (zappedNote.zapPayments.isNotEmpty()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val it = account.calculateZappedAmount(zappedNote)
|
||||
showAmount(it)
|
||||
val nwc = account.calculateZappedAmount(zappedNote)
|
||||
showAmount(nwc + java.math.BigDecimal(ownPendingOnchain))
|
||||
}
|
||||
} else {
|
||||
showAmount(zappedNote.zapsAmount)
|
||||
showAmount(zappedNote.zapsAmount + java.math.BigDecimal(ownPendingOnchain))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun calculateZapraiser(zappedNote: Note): ZapraiserStatus {
|
||||
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
|
||||
|
||||
@@ -769,12 +769,14 @@ open class Note(
|
||||
afterTimeInSeconds: Long,
|
||||
account: IAccount,
|
||||
): Boolean {
|
||||
// NIP-61 nutzaps: the sender is the source event's pubkey, so
|
||||
// the check is direct — no private-zap decryption needed (cashu
|
||||
// doesn't have a private-recipient variant the way NIP-57 does).
|
||||
// Run this first; it's an in-memory scan and a hit short-circuits
|
||||
// the more expensive lightning path.
|
||||
// NIP-61 nutzaps and NIP-BC onchain zaps: the sender is the
|
||||
// source event's pubkey, so the check is direct — no
|
||||
// private-zap decryption needed. Run these first; they're
|
||||
// in-memory scans and a hit short-circuits the more expensive
|
||||
// lightning path (which may have to decrypt NIP-44-private
|
||||
// zap requests).
|
||||
if (isNutzappedBy(user, afterTimeInSeconds)) return true
|
||||
if (isOnchainZappedBy(user, afterTimeInSeconds)) return true
|
||||
|
||||
val first = isZappedByCalculation(null, user, afterTimeInSeconds, account, zaps)
|
||||
if (first) return true
|
||||
@@ -793,6 +795,40 @@ open class Note(
|
||||
entry.source.author == user && sourceEvent.createdAt > afterTimeInSeconds
|
||||
}
|
||||
|
||||
private fun isOnchainZappedBy(
|
||||
user: User,
|
||||
afterTimeInSeconds: Long,
|
||||
): Boolean =
|
||||
onchainZaps.values.any { entry ->
|
||||
val sourceEvent = entry.source.event ?: return@any false
|
||||
entry.source.author == user && sourceEvent.createdAt > afterTimeInSeconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra sats to add on top of [zapsAmount] for the reaction-row
|
||||
* counter when the signed-in user has outgoing onchain zaps on
|
||||
* this note that aren't yet CONFIRMED. [updateZapTotal] only
|
||||
* counts CONFIRMED onchain entries (verifiedSats) because incoming
|
||||
* sender-claimed amounts are spoofable. The signed-in user's OWN
|
||||
* outgoing zap is trusted at face value though — they know what
|
||||
* they sent — so the counter should reflect it immediately, the
|
||||
* same way the gallery shows their own UNVERIFIED entry with the
|
||||
* claimed amount. Other senders' non-confirmed entries still
|
||||
* contribute 0 here.
|
||||
*/
|
||||
fun extraOwnPendingOnchainSats(loggedInPubKey: HexKey?): Long {
|
||||
if (loggedInPubKey == null) return 0L
|
||||
var sum = 0L
|
||||
onchainZaps.values.forEach { entry ->
|
||||
if (entry.status != OnchainZapStatus.CONFIRMED &&
|
||||
entry.source.author?.pubkeyHex == loggedInPubKey
|
||||
) {
|
||||
sum += entry.claimedSats
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
suspend fun isZappedBy(
|
||||
option: Int?,
|
||||
user: User,
|
||||
|
||||
Reference in New Issue
Block a user