Adding Zap Payments to the calculation of amounts and zapped by

This commit is contained in:
Vitor Pamplona
2023-04-30 21:30:30 -04:00
parent a38bbd7168
commit 446135ea2a
10 changed files with 153 additions and 26 deletions
@@ -20,6 +20,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import nostr.postr.Persona
import java.math.BigDecimal
import java.util.Locale
val DefaultChannels = setOf(
@@ -170,7 +171,26 @@ class Account(
return zapPaymentRequest != null
}
fun sendZapPaymentRequestFor(bolt11: String, onResponse: (Response?) -> Unit) {
fun decryptZapPaymentResponseEvent(zapResponseEvent: LnZapPaymentResponseEvent): Response? {
val myNip47 = zapPaymentRequest ?: return null
return zapResponseEvent.response(
myNip47.secret?.toByteArray() ?: loggedIn.privKey!!,
myNip47.pubKeyHex.toByteArray()
)
}
fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean {
return zappedNote.isZappedBy(userProfile(), this) == true
}
fun calculateZappedAmount(zappedNote: Note?): BigDecimal {
return zappedNote?.zappedAmount(
zapPaymentRequest?.secret?.toByteArray() ?: loggedIn.privKey!!,
zapPaymentRequest?.pubKeyHex?.toByteArray()
) ?: BigDecimal.ZERO
}
fun sendZapPaymentRequestFor(bolt11: String, zappedNote: Note?, onResponse: (Response?) -> Unit) {
if (!isWriteable()) return
zapPaymentRequest?.let { nip47 ->
@@ -179,7 +199,7 @@ class Account(
val wcListener = NostrLnZapPaymentResponseDataSource(nip47.pubKeyHex, event.pubKey, event.id)
wcListener.start()
LocalCache.consume(event) {
LocalCache.consume(event, zappedNote) {
// After the response is received.
val privKey = nip47.secret?.toByteArray()
if (privKey != null) {
@@ -4,7 +4,6 @@ import android.util.Log
import androidx.lifecycle.LiveData
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.service.NostrAccountDataSource.account
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledInsert
@@ -31,7 +30,8 @@ object LocalCache {
val channels = ConcurrentHashMap<HexKey, Channel>()
val addressables = ConcurrentHashMap<String, AddressableNote>(100)
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, (LnZapPaymentResponseEvent) -> Unit>(10)
val awaitingPaymentRequests =
ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
fun checkGetOrCreateUser(key: String): User? {
if (isValidHexNpub(key)) {
@@ -379,6 +379,7 @@ object LocalCache {
masterNote.removeBoost(deleteNote)
masterNote.removeReaction(deleteNote)
masterNote.removeZap(deleteNote)
masterNote.removeZapPayment(deleteNote)
masterNote.removeReport(deleteNote)
}
@@ -728,12 +729,41 @@ object LocalCache {
// Does nothing without a response callback.
}
fun consume(event: LnZapPaymentRequestEvent, onResponse: (LnZapPaymentResponseEvent) -> Unit) {
awaitingPaymentRequests.put(event.id, onResponse)
fun consume(event: LnZapPaymentRequestEvent, zappedNote: Note?, onResponse: (LnZapPaymentResponseEvent) -> Unit) {
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
zappedNote?.addZapPayment(note, null)
awaitingPaymentRequests.put(event.id, Pair(zappedNote, onResponse))
refreshObservers(note)
}
fun consume(event: LnZapPaymentResponseEvent) {
val responseCallback = awaitingPaymentRequests[event.requestId()]
val requestId = event.requestId()
val pair = awaitingPaymentRequests[requestId] ?: return
val (zappedNote, responseCallback) = pair
val requestNote = requestId?.let { checkGetOrCreateNote(requestId) }
val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey)
// Already processed this event.
if (note.event != null) return
note.loadEvent(event, author, emptyList())
requestNote?.let { request ->
zappedNote?.addZapPayment(request, note)
}
if (responseCallback != null) {
responseCallback(event)
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.EOSETime
@@ -45,6 +46,8 @@ open class Note(val idHex: String) {
private set
var zaps = mapOf<Note, Note?>()
private set
var zapPayments = mapOf<Note, Note?>()
private set
var relays = setOf<String>()
private set
@@ -156,6 +159,17 @@ open class Note(val idHex: String) {
}
}
fun removeZapPayment(note: Note) {
if (zapPayments[note] != null) {
zapPayments = zapPayments.minus(note)
liveSet?.zaps?.invalidateData()
} else if (zapPayments.containsValue(note)) {
val toRemove = zapPayments.filterValues { it == note }
zapPayments = zapPayments.minus(toRemove.keys)
liveSet?.zaps?.invalidateData()
}
}
fun addBoost(note: Note) {
if (note !in boosts) {
boosts = boosts + note
@@ -174,6 +188,17 @@ open class Note(val idHex: String) {
}
}
@Synchronized
fun addZapPayment(zapPaymentRequest: Note, zapPayment: Note?) {
if (zapPaymentRequest !in zapPayments.keys) {
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
liveSet?.zaps?.invalidateData()
} else if (zapPaymentRequest in zapPayments.keys && zapPayments[zapPaymentRequest] == null) {
zapPayments = zapPayments + Pair(zapPaymentRequest, zapPayment)
liveSet?.zaps?.invalidateData()
}
}
fun addReaction(note: Note) {
if (note !in reactions) {
reactions = reactions + note
@@ -204,6 +229,14 @@ open class Note(val idHex: String) {
// Zaps who the requester was the user
return zaps.any {
it.key.author === user || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex
} || zapPayments.any {
val zapResponseEvent = it.value?.event as? LnZapPaymentResponseEvent
val response = if (zapResponseEvent != null) {
account.decryptZapPaymentResponseEvent(zapResponseEvent)
} else {
null
}
response is PayInvoiceSuccessResponse && zapResponseEvent?.requestAuthor() == user.pubkeyHex
}
}
@@ -233,12 +266,44 @@ open class Note(val idHex: String) {
}.flatten()
}
fun zappedAmount(): BigDecimal {
return zaps.mapNotNull { it.value?.event }
fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
// Regular Zap Receipts
val completedZaps = zaps.asSequence()
.mapNotNull { it.value?.event }
.filterIsInstance<LnZapEvent>()
.mapNotNull {
it.amount
}.sumOf { it }
.filter { it.amount != null }
.associate {
it.lnInvoice() to it.amount
}
.toMap()
val completedPayments = if (privKey != null && walletServicePubkey != null) {
// Payments confirmed by the User's Wallet
zapPayments
.asSequence()
.filter {
val response = (it.value?.event as? LnZapPaymentResponseEvent)?.response(privKey, walletServicePubkey)
response is PayInvoiceSuccessResponse
}
.associate {
val lnInvoice = (it.key.event as? LnZapPaymentRequestEvent)?.lnInvoice(privKey)
val amount = try {
if (lnInvoice == null) {
null
} else {
LnInvoiceUtil.getAmountInSats(lnInvoice)
}
} catch (e: java.lang.Exception) {
null
}
lnInvoice to amount
}
.toMap()
} else {
emptyMap()
}
return (completedZaps + completedPayments).values.filterNotNull().sumOf { it }
}
fun hasPledgeBy(user: User): Boolean {
@@ -19,9 +19,14 @@ class FileHeader(
) {
companion object {
fun prepare(fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
val imageData = URL(fileUrl).readBytes()
try {
val imageData = URL(fileUrl).readBytes()
prepare(imageData, fileUrl, mimeType, description, onReady, onError)
prepare(imageData, fileUrl, mimeType, description, onReady, onError)
} catch (e: Exception) {
Log.e("ImageDownload", "Couldn't download image from server: ${e.message}")
onError()
}
}
fun prepare(data: ByteArray, fileUrl: String, mimeType: String?, description: String?, onReady: (FileHeader) -> Unit, onError: () -> Unit) {
@@ -57,7 +57,7 @@ class LnZapEvent(
return content
}
private fun lnInvoice() = tags.firstOrNull { it.size > 1 && it[0] == "bolt11" }?.get(1)
fun lnInvoice() = tags.firstOrNull { it.size > 1 && it[0] == "bolt11" }?.get(1)
private fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
@@ -56,8 +56,7 @@ fun PollNote(
val account = accountState?.account ?: return
val pollViewModel = PollNoteViewModel()
pollViewModel.account = account
pollViewModel.load(zappedNote)
pollViewModel.load(account, zappedNote)
pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op ->
OptionNote(
@@ -212,7 +211,7 @@ fun ZapVote(
clickablePrepend: @Composable () -> Unit
) {
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note
val zappedNote = zapsState?.note ?: return
var wantsToZap by remember { mutableStateOf(false) }
@@ -253,7 +252,7 @@ fun ZapVote(
)
.show()
}
} else if (accountViewModel.isLoggedUser(zappedNote?.author)) {
} else if (accountViewModel.isLoggedUser(zappedNote.author)) {
scope.launch {
Toast
.makeText(
@@ -376,7 +375,7 @@ fun ZapVote(
LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) {
if (!wasZappedByLoggedInUser) {
wasZappedByLoggedInUser = zappedNote?.isZappedBy(accountViewModel.userProfile(), account) == true
wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
}
}
}
@@ -20,8 +20,10 @@ class PollNoteViewModel {
var consensusThreshold: BigDecimal? = null
var totalZapped: BigDecimal = BigDecimal.ZERO
var wasZappedByAuthor: Boolean = false
fun load(note: Note?) {
fun load(acc: Account, note: Note?) {
account = acc
pollNote = note
pollEvent = pollNote?.event as PollNoteEvent
pollOptions = pollEvent?.pollOptions()
@@ -31,13 +33,14 @@ class PollNoteViewModel {
closedAt = pollEvent?.getTagInt(CLOSED_AT)
totalZapped = totalZapped()
wasZappedByAuthor = note?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
}
fun canZap(): Boolean {
val account = account ?: return false
val user = account.userProfile() ?: return false
val note = pollNote ?: return false
return user != note.author && !note.isZappedBy(user, account)
return user != note.author && !wasZappedByAuthor
}
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
@@ -309,7 +309,7 @@ fun ZapReaction(
val account = accountState?.account ?: return
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note
val zappedNote = zapsState?.note ?: return
val zapMessage = ""
var wantsToZap by remember { mutableStateOf(false) }
@@ -326,7 +326,7 @@ fun ZapReaction(
LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) {
if (!wasZappedByLoggedInUser) {
wasZappedByLoggedInUser = zappedNote?.isZappedBy(account.userProfile(), account) == true
wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
}
}
}
@@ -459,7 +459,7 @@ fun ZapReaction(
LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) {
zapAmount = zappedNote?.zappedAmount()
zapAmount = account.calculateZappedAmount(zappedNote)
}
}
@@ -59,6 +59,10 @@ class AccountViewModel(private val account: Account) : ViewModel() {
zap(note, amount, pollOption, message, context, onError, onProgress, account.defaultZapType)
}
fun calculateIfNoteWasZappedByAccount(zappedNote: Note): Boolean {
return account.calculateIfNoteWasZappedByAccount(zappedNote)
}
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
@@ -88,6 +92,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
if (account.hasWalletConnectSetup()) {
account.sendZapPaymentRequestFor(
bolt11 = it,
note,
onResponse = { response ->
if (response is PayInvoiceErrorResponse) {
onProgress(0.0f)
@@ -565,7 +565,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
onSuccess = {
// pay directly
if (account.hasWalletConnectSetup()) {
account.sendZapPaymentRequestFor(it) { response ->
account.sendZapPaymentRequestFor(it, null) { response ->
if (response is PayInvoiceSuccessResponse) {
scope.launch {
Toast.makeText(