Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Believethehype
2023-05-01 18:59:38 +02:00
11 changed files with 203 additions and 27 deletions
@@ -20,6 +20,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.Utils
import java.math.BigDecimal
import java.util.Locale import java.util.Locale
val DefaultChannels = setOf( val DefaultChannels = setOf(
@@ -170,7 +172,32 @@ class Account(
return zapPaymentRequest != null return zapPaymentRequest != null
} }
fun sendZapPaymentRequestFor(bolt11: String, onResponse: (Response?) -> Unit) { fun isNIP47Author(pubkeyHex: String?): Boolean {
val privKey = zapPaymentRequest?.secret?.toByteArray() ?: loggedIn.privKey!!
val pubKey = Utils.pubkeyCreate(privKey).toHexKey()
return (pubKey == pubkeyHex)
}
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 if (!isWriteable()) return
zapPaymentRequest?.let { nip47 -> zapPaymentRequest?.let { nip47 ->
@@ -179,11 +206,11 @@ class Account(
val wcListener = NostrLnZapPaymentResponseDataSource(nip47.pubKeyHex, event.pubKey, event.id) val wcListener = NostrLnZapPaymentResponseDataSource(nip47.pubKeyHex, event.pubKey, event.id)
wcListener.start() wcListener.start()
LocalCache.consume(event) { LocalCache.consume(event, zappedNote) {
// After the response is received. // After the response is received.
val privKey = nip47.secret?.toByteArray() val privKey = nip47.secret?.toByteArray()
if (privKey != null) { if (privKey != null) {
onResponse(it.response(privKey, event.pubKey.toByteArray())) onResponse(it.response(privKey, nip47.pubKeyHex.toByteArray()))
} }
} }
@@ -4,7 +4,6 @@ import android.util.Log
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 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.model.*
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledInsert import com.vitorpamplona.amethyst.ui.components.BundledInsert
@@ -31,7 +30,8 @@ object LocalCache {
val channels = ConcurrentHashMap<HexKey, Channel>() val channels = ConcurrentHashMap<HexKey, Channel>()
val addressables = ConcurrentHashMap<String, AddressableNote>(100) 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? { fun checkGetOrCreateUser(key: String): User? {
if (isValidHexNpub(key)) { if (isValidHexNpub(key)) {
@@ -379,6 +379,7 @@ object LocalCache {
masterNote.removeBoost(deleteNote) masterNote.removeBoost(deleteNote)
masterNote.removeReaction(deleteNote) masterNote.removeReaction(deleteNote)
masterNote.removeZap(deleteNote) masterNote.removeZap(deleteNote)
masterNote.removeZapPayment(deleteNote)
masterNote.removeReport(deleteNote) masterNote.removeReport(deleteNote)
} }
@@ -728,12 +729,41 @@ object LocalCache {
// Does nothing without a response callback. // Does nothing without a response callback.
} }
fun consume(event: LnZapPaymentRequestEvent, onResponse: (LnZapPaymentResponseEvent) -> Unit) { fun consume(event: LnZapPaymentRequestEvent, zappedNote: Note?, onResponse: (LnZapPaymentResponseEvent) -> Unit) {
awaitingPaymentRequests.put(event.id, onResponse) 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) { 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) { if (responseCallback != null) {
responseCallback(event) responseCallback(event)
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.nip19.Nip19 import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.EOSETime import com.vitorpamplona.amethyst.service.relays.EOSETime
@@ -45,6 +46,8 @@ open class Note(val idHex: String) {
private set private set
var zaps = mapOf<Note, Note?>() var zaps = mapOf<Note, Note?>()
private set private set
var zapPayments = mapOf<Note, Note?>()
private set
var relays = setOf<String>() var relays = setOf<String>()
private set 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) { fun addBoost(note: Note) {
if (note !in boosts) { if (note !in boosts) {
boosts = boosts + note 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) { fun addReaction(note: Note) {
if (note !in reactions) { if (note !in reactions) {
reactions = reactions + note reactions = reactions + note
@@ -204,6 +229,14 @@ open class Note(val idHex: String) {
// Zaps who the requester was the user // Zaps who the requester was the user
return zaps.any { return zaps.any {
it.key.author === user || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex 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 && account.isNIP47Author(zapResponseEvent?.requestAuthor())
} }
} }
@@ -233,12 +266,44 @@ open class Note(val idHex: String) {
}.flatten() }.flatten()
} }
fun zappedAmount(): BigDecimal { fun zappedAmount(privKey: ByteArray?, walletServicePubkey: ByteArray?): BigDecimal {
return zaps.mapNotNull { it.value?.event } // Regular Zap Receipts
val completedZaps = zaps.asSequence()
.mapNotNull { it.value?.event }
.filterIsInstance<LnZapEvent>() .filterIsInstance<LnZapEvent>()
.mapNotNull { .filter { it.amount != null }
it.amount .associate {
}.sumOf { it } 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 { fun hasPledgeBy(user: User): Boolean {
@@ -207,6 +207,7 @@ open class Event(
.registerTypeAdapter(Event::class.java, EventDeserializer()) .registerTypeAdapter(Event::class.java, EventDeserializer())
.registerTypeAdapter(ByteArray::class.java, ByteArraySerializer()) .registerTypeAdapter(ByteArray::class.java, ByteArraySerializer())
.registerTypeAdapter(ByteArray::class.java, ByteArrayDeserializer()) .registerTypeAdapter(ByteArray::class.java, ByteArrayDeserializer())
.registerTypeAdapter(Response::class.java, ResponseDeserializer())
.create() .create()
fun fromJson(json: String, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient) fun fromJson(json: String, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
@@ -57,7 +57,7 @@ class LnZapEvent(
return content 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) private fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
@@ -1,9 +1,14 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.lang.reflect.Type
class LnZapPaymentResponseEvent( class LnZapPaymentResponseEvent(
id: HexKey, id: HexKey,
@@ -35,8 +40,13 @@ class LnZapPaymentResponseEvent(
} }
fun response(privKey: ByteArray, pubKey: ByteArray): Response? = try { fun response(privKey: ByteArray, pubKey: ByteArray): Response? = try {
println("Response Arrived: Decrypting")
if (content.isNotEmpty()) { if (content.isNotEmpty()) {
gson.fromJson(decrypt(privKey, pubKey), Response::class.java) val decrypted = decrypt(privKey, pubKey)
println("Response Arrived: Parsing $decrypted")
val response = gson.fromJson(decrypted, Response::class.java)
println("Response Arrived: Decrypted ${response?.resultType}")
response
} else { } else {
null null
} }
@@ -58,7 +68,7 @@ abstract class Response(
// PayInvoice Call // PayInvoice Call
class PayInvoiceSuccessResponse(val result: PayInvoiceResultParams) : class PayInvoiceSuccessResponse(val result: PayInvoiceResultParams? = null) :
Response("pay_invoice") { Response("pay_invoice") {
class PayInvoiceResultParams(val preimage: String) class PayInvoiceResultParams(val preimage: String)
} }
@@ -86,3 +96,39 @@ class PayInvoiceErrorResponse(val error: PayInvoiceErrorParams? = null) :
OTHER // Other error. OTHER // Other error.
} }
} }
class ResponseDeserializer :
JsonDeserializer<Response?> {
@Throws(JsonParseException::class)
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): Response? {
val jsonObject = json.asJsonObject
val resultType = jsonObject.get("result_type")?.asString
if (resultType == "pay_invoice") {
val result = jsonObject.get("result")?.asJsonObject
val error = jsonObject.get("error")?.asJsonObject
if (result != null) {
return context.deserialize<PayInvoiceSuccessResponse>(jsonObject, PayInvoiceSuccessResponse::class.java)
}
if (error != null) {
return context.deserialize<PayInvoiceErrorResponse>(jsonObject, PayInvoiceErrorResponse::class.java)
}
} else {
// tries to guess
if (jsonObject.get("result")?.asJsonObject?.get("preimage") != null) {
return context.deserialize<PayInvoiceSuccessResponse>(jsonObject, PayInvoiceSuccessResponse::class.java)
}
if (jsonObject.get("error")?.asJsonObject?.get("code") != null) {
return context.deserialize<PayInvoiceErrorResponse>(jsonObject, PayInvoiceErrorResponse::class.java)
}
}
return null
}
companion object {
}
}
@@ -56,8 +56,7 @@ fun PollNote(
val account = accountState?.account ?: return val account = accountState?.account ?: return
val pollViewModel = PollNoteViewModel() val pollViewModel = PollNoteViewModel()
pollViewModel.account = account pollViewModel.load(account, zappedNote)
pollViewModel.load(zappedNote)
pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op -> pollViewModel.pollEvent?.pollOptions()?.forEach { poll_op ->
OptionNote( OptionNote(
@@ -212,7 +211,7 @@ fun ZapVote(
clickablePrepend: @Composable () -> Unit clickablePrepend: @Composable () -> Unit
) { ) {
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note val zappedNote = zapsState?.note ?: return
var wantsToZap by remember { mutableStateOf(false) } var wantsToZap by remember { mutableStateOf(false) }
@@ -253,7 +252,7 @@ fun ZapVote(
) )
.show() .show()
} }
} else if (accountViewModel.isLoggedUser(zappedNote?.author)) { } else if (accountViewModel.isLoggedUser(zappedNote.author)) {
scope.launch { scope.launch {
Toast Toast
.makeText( .makeText(
@@ -376,7 +375,7 @@ fun ZapVote(
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!wasZappedByLoggedInUser) { if (!wasZappedByLoggedInUser) {
wasZappedByLoggedInUser = zappedNote?.isZappedBy(accountViewModel.userProfile(), account) == true wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
} }
} }
} }
@@ -20,8 +20,10 @@ class PollNoteViewModel {
var consensusThreshold: BigDecimal? = null var consensusThreshold: BigDecimal? = null
var totalZapped: BigDecimal = BigDecimal.ZERO var totalZapped: BigDecimal = BigDecimal.ZERO
var wasZappedByAuthor: Boolean = false
fun load(note: Note?) { fun load(acc: Account, note: Note?) {
account = acc
pollNote = note pollNote = note
pollEvent = pollNote?.event as PollNoteEvent pollEvent = pollNote?.event as PollNoteEvent
pollOptions = pollEvent?.pollOptions() pollOptions = pollEvent?.pollOptions()
@@ -31,13 +33,14 @@ class PollNoteViewModel {
closedAt = pollEvent?.getTagInt(CLOSED_AT) closedAt = pollEvent?.getTagInt(CLOSED_AT)
totalZapped = totalZapped() totalZapped = totalZapped()
wasZappedByAuthor = note?.let { account?.calculateIfNoteWasZappedByAccount(it) } ?: false
} }
fun canZap(): Boolean { fun canZap(): Boolean {
val account = account ?: return false val account = account ?: return false
val user = account.userProfile() ?: return false val user = account.userProfile() ?: return false
val note = pollNote ?: 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 fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
@@ -309,7 +309,7 @@ fun ZapReaction(
val account = accountState?.account ?: return val account = accountState?.account ?: return
val zapsState by baseNote.live().zaps.observeAsState() val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note val zappedNote = zapsState?.note ?: return
val zapMessage = "" val zapMessage = ""
var wantsToZap by remember { mutableStateOf(false) } var wantsToZap by remember { mutableStateOf(false) }
@@ -326,7 +326,7 @@ fun ZapReaction(
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!wasZappedByLoggedInUser) { if (!wasZappedByLoggedInUser) {
wasZappedByLoggedInUser = zappedNote?.isZappedBy(account.userProfile(), account) == true wasZappedByLoggedInUser = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
} }
} }
} }
@@ -459,7 +459,7 @@ fun ZapReaction(
LaunchedEffect(key1 = zapsState) { LaunchedEffect(key1 = zapsState) {
withContext(Dispatchers.IO) { 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) 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) { 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() 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()) { if (account.hasWalletConnectSetup()) {
account.sendZapPaymentRequestFor( account.sendZapPaymentRequestFor(
bolt11 = it, bolt11 = it,
note,
onResponse = { response -> onResponse = { response ->
if (response is PayInvoiceErrorResponse) { if (response is PayInvoiceErrorResponse) {
onProgress(0.0f) onProgress(0.0f)
@@ -565,7 +565,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
onSuccess = { onSuccess = {
// pay directly // pay directly
if (account.hasWalletConnectSetup()) { if (account.hasWalletConnectSetup()) {
account.sendZapPaymentRequestFor(it) { response -> account.sendZapPaymentRequestFor(it, null) { response ->
if (response is PayInvoiceSuccessResponse) { if (response is PayInvoiceSuccessResponse) {
scope.launch { scope.launch {
Toast.makeText( Toast.makeText(