diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt index 049f847d..97cc971a 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -12,6 +12,8 @@ import com.greenart7c3.nostrsigner.models.ReturnType import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.vitorpamplona.quartz.crypto.KeyPair +import com.vitorpamplona.quartz.encoders.toHexKey +import com.vitorpamplona.quartz.events.Event import java.net.URLDecoder object IntentUtils { @@ -69,11 +71,21 @@ object IntentUtils { } return null } - fun getIntent(data: String, keyPair: KeyPair): AmberEvent { - var event = AmberEvent.fromJson(data) + fun getIntent(data: String, keyPair: KeyPair): Event { + val event = AmberEvent.fromJson(data) if (event.pubKey.isEmpty()) { - event = AmberEvent.setPubKeyIfEmpty(event, keyPair) + event.pubKey = keyPair.pubKey.toHexKey() } - return event + if (event.id.isEmpty()) { + event.id = Event.generateId( + event.pubKey, + event.createdAt, + event.kind, + event.tags, + event.content + ).toHexKey() + } + + return AmberEvent.toEvent(event) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt index ffe945a3..dbae98d1 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt @@ -1,6 +1,5 @@ package com.greenart7c3.nostrsigner.service.model -import android.util.Log import androidx.compose.runtime.Immutable import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -9,228 +8,25 @@ import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer import com.google.gson.JsonElement import com.google.gson.JsonObject -import com.google.gson.JsonParseException -import com.google.gson.JsonPrimitive import com.google.gson.JsonSerializationContext import com.google.gson.JsonSerializer import com.google.gson.annotations.SerializedName import com.greenart7c3.nostrsigner.models.TimeUtils -import com.vitorpamplona.quartz.crypto.CryptoUtils -import com.vitorpamplona.quartz.crypto.KeyPair -import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey -import com.vitorpamplona.quartz.encoders.toHexKey -import com.vitorpamplona.quartz.events.EmojiUrl import com.vitorpamplona.quartz.events.Event -import fr.acinq.secp256k1.Hex import java.lang.reflect.Type -import java.math.BigDecimal import java.util.* @Immutable open class AmberEvent( - val id: HexKey, - @SerializedName("pubkey") val pubKey: HexKey, + var id: HexKey, + @SerializedName("pubkey") var pubKey: HexKey, @SerializedName("created_at") val createdAt: Long, val kind: Int, val tags: Array>, val content: String, val sig: HexKey -) : AmberEventInterface { - override fun id(): HexKey = id - - override fun pubKey(): HexKey = pubKey - - override fun createdAt(): Long = createdAt - - override fun kind(): Int = kind - override fun description(): String { - return "event" - } - - override fun tags(): Array> = tags - - override fun content(): String = content - - override fun sig(): HexKey = sig - - override fun toJson(): String = gson.toJson(this) - - fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" } - - override fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } - override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } - - override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } - - override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) } - - fun relay() = tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first() - - override fun isSensitive() = tags.any { - (it.size > 0 && it[0].equals("content-warning", true)) || - (it.size > 1 && it[0] == "t" && it[1].equals("nsfw", true)) || - (it.size > 1 && it[0] == "t" && it[1].equals("nude", true)) - } - - override fun zapraiserAmount() = tags.firstOrNull() { - (it.size > 1 && it[0] == "zapraiser") - }?.get(1)?.toLongOrNull() - - override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1) - - override fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { - val aTagValue = it[1] - val relay = it.getOrNull(2) - - ATag.parse(aTagValue, relay) - } - - override fun hashtags() = tags.filter { it.size > 1 && it[0] == "t" }.map { it[1] } - override fun geohashes() = tags.filter { it.size > 1 && it[0] == "g" }.map { it[1] } - - override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) } - - override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex } - - override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex } - - override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex } - - override fun isTaggedAddressableNotes(idHexes: Set) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes } - - override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) } - - override fun isTaggedGeoHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "g" && it[1].startsWith(hashtag, true) } - override fun isTaggedHashes(hashtags: Set) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags } - override fun isTaggedGeoHashes(hashtags: Set) = tags.any { it.size > 1 && it[0] == "g" && it[1].lowercase() in hashtags } - override fun firstIsTaggedHashes(hashtags: Set) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1) - - override fun firstIsTaggedAddressableNote(addressableNotes: Set) = tags.firstOrNull { it.size > 1 && it[0] == "a" && it[1] in addressableNotes }?.getOrNull(1) - - override fun isTaggedAddressableKind(kind: Int): Boolean { - val kindStr = kind.toString() - return tags.any { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) } - } - - override fun getTagOfAddressableKind(kind: Int): ATag? { - val kindStr = kind.toString() - val aTag = tags - .firstOrNull { it.size > 1 && it[0] == "a" && it[1].startsWith(kindStr) } - ?.getOrNull(1) - ?: return null - - return ATag.parse(aTag, null) - } - - override fun getPoWRank(): Int { - var rank = 0 - for (i in 0..id.length) { - if (id[i] == '0') { - rank += 4 - } else if (id[i] in '4'..'7') { - rank += 1 - break - } else if (id[i] in '2'..'3') { - rank += 2 - break - } else if (id[i] == '1') { - rank += 3 - break - } else { - break - } - } - return rank - } - - private fun isHex2(c: Char): Boolean { - return when (c) { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', ' ' -> true - else -> false - } - } - - private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) - - fun verifiedRecipientPubKey(): HexKey? { - val recipient = recipientPubKey() - return if (isHex(recipient)) { - recipient - } else { - null - } - } - - fun isHex(hex: String?): Boolean { - if (hex == null) return false - var isHex = true - for (c in hex.toCharArray()) { - if (!isHex2(c)) { - isHex = false - break - } - } - return isHex - } - - fun talkingWith(oneSideHex: String): HexKey { - return if (pubKey == oneSideHex) verifiedRecipientPubKey() ?: pubKey else pubKey - } - - override fun getGeoHash(): String? { - return tags.firstOrNull { it.size > 1 && it[0] == "g" }?.get(1)?.ifBlank { null } - } - - override fun getReward(): BigDecimal? { - return try { - tags.firstOrNull { it.size > 1 && it[0] == "reward" }?.get(1)?.let { BigDecimal(it) } - } catch (e: Exception) { - null - } - } - - /** - * Checks if the ID is correct and then if the pubKey's secret key signed the event. - */ - override fun checkSignature() { - if (!id.contentEquals(generateId())) { - throw Exception( - """|Unexpected ID. - | Event: ${toJson()} - | Actual ID: $id - | Generated: ${generateId()} - """.trimIndent() - ) - } - if (!CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) { - throw Exception("""Bad signature!""") - } - } - - override fun hasValidSignature(): Boolean { - return try { - id.contentEquals(generateId()) && CryptoUtils.verifySignature(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey)) - } catch (e: Exception) { - Log.e("Event", "Fail checking if event $id has a valid signature", e) - false - } - } - - private fun generateId(): String { - val rawEvent = listOf(0, pubKey, createdAt, kind, tags, content) - - // GSON decided to hardcode these replacements. - // They break Nostr's hash check. - // These lines revert their code. - // https://github.com/google/gson/issues/2295 - val rawEventJson = gson.toJson(rawEvent) - .replace("\\u2028", "\u2028") - .replace("\\u2029", "\u2029") - - return CryptoUtils.sha256(rawEventJson.toByteArray()).toHexKey() - } - +) { private class EventDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement, @@ -283,144 +79,14 @@ open class AmberEvent( } } - private class ByteArrayDeserializer : JsonDeserializer { - override fun deserialize( - json: JsonElement, - typeOfT: Type?, - context: JsonDeserializationContext? - ): ByteArray = Hex.decode(json.asString) - } - - abstract class Response( - @SerializedName("result_type") - val resultType: String - ) - -// PayInvoice Call - - class PayInvoiceSuccessResponse(val result: PayInvoiceResultParams? = null) : - Response("pay_invoice") { - class PayInvoiceResultParams(val preimage: String) - } - - class PayInvoiceErrorResponse(val error: PayInvoiceErrorParams? = null) : - Response("pay_invoice") { - class PayInvoiceErrorParams(val code: ErrorType?, val message: String?) - - enum class ErrorType { - @SerializedName(value = "rate_limited", alternate = ["RATE_LIMITED"]) - RATE_LIMITED, // The client is sending commands too fast. It should retry in a few seconds. - @SerializedName(value = "not_implemented", alternate = ["NOT_IMPLEMENTED"]) - NOT_IMPLEMENTED, // The command is not known or is intentionally not implemented. - @SerializedName(value = "insufficient_balance", alternate = ["INSUFFICIENT_BALANCE"]) - INSUFFICIENT_BALANCE, // The wallet does not have enough funds to cover a fee reserve or the payment amount. - @SerializedName(value = "quota_exceeded", alternate = ["QUOTA_EXCEEDED"]) - QUOTA_EXCEEDED, // The wallet has exceeded its spending quota. - @SerializedName(value = "restricted", alternate = ["RESTRICTED"]) - RESTRICTED, // This public key is not allowed to do this operation. - @SerializedName(value = "unauthorized", alternate = ["UNAUTHORIZED"]) - UNAUTHORIZED, // This public key has no wallet connected. - @SerializedName(value = "internal", alternate = ["INTERNAL"]) - INTERNAL, // An internal error. - @SerializedName(value = "other", alternate = ["OTHER"]) - OTHER // Other error. - } - } - - class ResponseDeserializer : - JsonDeserializer { - @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(jsonObject, PayInvoiceSuccessResponse::class.java) - } - if (error != null) { - return context.deserialize(jsonObject, PayInvoiceErrorResponse::class.java) - } - } else { - // tries to guess - if (jsonObject.get("result")?.asJsonObject?.get("preimage") != null) { - return context.deserialize(jsonObject, PayInvoiceSuccessResponse::class.java) - } - if (jsonObject.get("error")?.asJsonObject?.get("code") != null) { - return context.deserialize(jsonObject, PayInvoiceErrorResponse::class.java) - } - } - return null - } - - companion object { - } - } - - abstract class Request(var method: String? = null) - - class PayInvoiceParams(var invoice: String? = null) - - class PayInvoiceMethod(var params: PayInvoiceParams? = null) : Request("pay_invoice") { - - companion object { - fun create(bolt11: String): PayInvoiceMethod { - return PayInvoiceMethod(PayInvoiceParams(bolt11)) - } - } - } - - class RequestDeserializer : - JsonDeserializer { - @Throws(JsonParseException::class) - override fun deserialize( - json: JsonElement, - typeOfT: Type, - context: JsonDeserializationContext - ): Request? { - val jsonObject = json.asJsonObject - val method = jsonObject.get("method")?.asString - - if (method == "pay_invoice") { - return context.deserialize(jsonObject, PayInvoiceMethod::class.java) - } - return null - } - - companion object { - } - } - - private class ByteArraySerializer : JsonSerializer { - override fun serialize( - src: ByteArray, - typeOfSrc: Type?, - context: JsonSerializationContext? - ) = JsonPrimitive(src.toHexKey()) - } - companion object { val gson: Gson = GsonBuilder() .disableHtmlEscaping() .registerTypeAdapter(AmberEvent::class.java, EventSerializer()) .registerTypeAdapter(AmberEvent::class.java, EventDeserializer()) - .registerTypeAdapter(ByteArray::class.java, ByteArraySerializer()) - .registerTypeAdapter(ByteArray::class.java, ByteArrayDeserializer()) - .registerTypeAdapter(Response::class.java, ResponseDeserializer()) - .registerTypeAdapter(Request::class.java, RequestDeserializer()) .create() - fun fromJson(json: String): AmberEvent = gson.fromJson(json, AmberEvent::class.java).getRefinedEvent() - - fun AmberEvent.getRefinedEvent(): AmberEvent = when (kind) { - else -> this - } + fun fromJson(json: String): AmberEvent = gson.fromJson(json, AmberEvent::class.java) fun toEvent(amberEvent: AmberEvent): Event { return Event( @@ -434,49 +100,8 @@ open class AmberEvent( ) } - fun generateId(pubKey: HexKey, createdAt: Long, kind: Int, tags: Array>, content: String): ByteArray { - val rawEvent = listOf( - 0, - pubKey, - createdAt, - kind, - tags, - content - ) - - // GSON decided to hardcode these replacements. - // They break Nostr's hash check. - // These lines revert their code. - // https://github.com/google/gson/issues/2295 - val rawEventJson = gson.toJson(rawEvent) - .replace("\\u2028", "\u2028") - .replace("\\u2029", "\u2029") - - return CryptoUtils.sha256(rawEventJson.toByteArray()) - } - - fun setPubKeyIfEmpty(event: AmberEvent, keyPair: KeyPair): AmberEvent { - if (event.pubKey.isEmpty()) { - val pubKey = keyPair.pubKey.toHexKey() - val id = generateId(pubKey, event.createdAt, event.kind, event.tags, event.content) - val signature = CryptoUtils.sign(id, keyPair.privKey!!).toHexKey() - return AmberEvent(id.toHexKey(), pubKey, event.createdAt, event.kind, event.tags, event.content, signature) - } - - return event - } - - fun create(privateKey: ByteArray, kind: Int, tags: Array> = emptyArray(), content: String = "", createdAt: Long = TimeUtils.now()): AmberEvent { - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() - val id = generateId(pubKey, createdAt, kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey).toHexKey() - return AmberEvent(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig) + fun relay(event: Event): String { + return event.tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.first() } } } - -@Immutable -interface AddressableEvent { - fun dTag(): String - fun address(): ATag -} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt index 8496577a..c859eb7d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/EventData.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.service.model.AmberEvent +import com.vitorpamplona.quartz.events.Event @Composable fun EventData( @@ -32,7 +33,7 @@ fun EventData( packageName: String?, appName: String, applicationName: String?, - event: AmberEvent, + event: Event, rawJson: String, onAccept: () -> Unit, onReject: () -> Unit @@ -66,7 +67,7 @@ fun EventData( "Event content", fontWeight = FontWeight.Bold ) - val content = if (event.kind == 22242) event.relay() else event.content + val content = if (event.kind == 22242) AmberEvent.relay(event) else event.content Text( content, maxLines = 1, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt index 21e76c8d..551356ae 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/MultiEventHomeScreen.kt @@ -46,6 +46,7 @@ import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.service.AmberUtils import com.greenart7c3.nostrsigner.service.IntentUtils import com.greenart7c3.nostrsigner.service.getAppCompatActivity +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.Result import com.greenart7c3.nostrsigner.ui.theme.ButtonBorder @@ -187,7 +188,7 @@ fun MultiEventHomeScreen( intentData.data, accountParam.keyPair ) - if (event.kind == 22242) event.relay() else event.content + if (event.kind == 22242) AmberEvent.relay(event) else event.content } else { intentData.data } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt index 4fda67bd..680b4772 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/previews/EventDataPreview.kt @@ -10,15 +10,15 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.greenart7c3.nostrsigner.models.TimeUtils -import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.ui.components.EventData import com.greenart7c3.nostrsigner.ui.theme.NostrSignerTheme +import com.vitorpamplona.quartz.events.Event @SuppressLint("UnusedMaterialScaffoldPaddingParameter") @Preview(device = "id:Nexus S") @Composable fun EventDataPreview() { - val event = AmberEvent("123", "7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19", TimeUtils.now(), 1, arrayOf(), "This is a test 123 123 123 123 123 123", "") + val event = Event("123", "7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19", TimeUtils.now(), 1, arrayOf(), "This is a test 123 123 123 123 123 123", "") val data = event.toJson() val remember = remember { mutableStateOf(false) }