refactor(nwc): validate zapped a/e tags with the existing Address and HexKey helpers

This commit is contained in:
davotoula
2026-09-15 15:21:39 +02:00
parent 29fb6760df
commit d79b933274
2 changed files with 20 additions and 39 deletions
@@ -20,10 +20,11 @@
*/
package com.vitorpamplona.quartz.nip47WalletConnect.rpc
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.RawJson
import com.vitorpamplona.quartz.nip01Core.core.isValid
import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull
import com.vitorpamplona.quartz.utils.Hex
class NwcTransactionMetadata(
val comment: String?,
@@ -45,8 +46,8 @@ class NwcTransactionMetadata(
val pubkeyHex: String?,
val recipientPubkeyHex: String?,
val content: String?,
val zappedEventId: String? = null,
val zappedAddress: String? = null,
val zappedEventId: String?,
val zappedAddress: String?,
)
fun senderPubkeyHex(): String? = nostr?.pubkeyHex ?: payerData?.pubkey?.let { decodePublicKeyAsHexOrNull(it) }
@@ -109,8 +110,8 @@ class NwcTransactionMetadata(
val value = tagList[1] as? String ?: return@forEach
when (tagList[0]) {
"p" -> if (recipientHex == null) recipientHex = value
"e" -> if (zappedEventId == null && value.length == 64 && Hex.isHex64(value)) zappedEventId = value
"a" -> if (zappedAddress == null) zappedAddress = value
"e" -> if (zappedEventId == null && value.isValid()) zappedEventId = value
"a" -> if (zappedAddress == null) zappedAddress = Address.parse(value)?.toValue()
}
}
@@ -254,23 +254,16 @@ class NwcOutgoingMetadataTest {
assertEquals(recipientHex, parsed.recipientPubkeyHex(), "the p tag is still read with the e tag in front of it")
}
private fun parseTags(vararg tags: List<String>) = assertNotNull(NwcTransactionMetadata.parse(mapOf("nostr" to mapOf("tags" to tags.toList()))))
@Test
fun aProfileZapHasNoNote() {
val parsed =
assertNotNull(
NwcTransactionMetadata.parse(mapOf("nostr" to mapOf("tags" to listOf(listOf("p", recipientHex))))),
)
assertNull(parsed.zappedNoteId())
assertNull(parseTags(listOf("p", recipientHex)).zappedNoteId())
}
@Test
fun tagOrderDoesNotMatter() {
val parsed =
assertNotNull(
NwcTransactionMetadata.parse(
mapOf("nostr" to mapOf("tags" to listOf(listOf("e", noteHex), listOf("p", recipientHex)))),
),
)
val parsed = parseTags(listOf("e", noteHex), listOf("p", recipientHex))
assertEquals(recipientHex, parsed.recipientPubkeyHex())
assertEquals(noteHex, parsed.zappedNoteId())
}
@@ -278,33 +271,20 @@ class NwcOutgoingMetadataTest {
@Test
fun anAddressableZapPrefersTheAddress() {
val address = "30023:$recipientHex:my-article"
val parsed =
assertNotNull(
NwcTransactionMetadata.parse(
mapOf("nostr" to mapOf("tags" to listOf(listOf("e", noteHex), listOf("a", address), listOf("p", recipientHex)))),
),
)
assertEquals(address, parsed.zappedNoteId())
assertEquals(address, parseTags(listOf("e", noteHex), listOf("a", address), listOf("p", recipientHex)).zappedNoteId())
}
@Test
fun aMalformedAddressFallsBackToTheEventId() {
assertEquals(noteHex, parseTags(listOf("e", noteHex), listOf("a", "not-an-address"), listOf("p", recipientHex)).zappedNoteId())
}
@Test
fun aMalformedEventIdIsIgnored() {
val parsed =
assertNotNull(
NwcTransactionMetadata.parse(
mapOf("nostr" to mapOf("tags" to listOf(listOf("e", "not-a-note"), listOf("p", recipientHex)))),
),
)
assertNull(parsed.zappedNoteId())
// isHex64 checks only the first 64 chars; a longer id must not slip through.
val tooLong =
assertNotNull(
NwcTransactionMetadata.parse(
mapOf("nostr" to mapOf("tags" to listOf(listOf("e", noteHex + "ff"), listOf("p", recipientHex)))),
),
)
assertNull(tooLong.zappedNoteId())
// "noteHex + ff": the id check must bound the length, not just scan 64 hex chars.
listOf("not-a-note", noteHex + "ff").forEach { bad ->
assertNull(parseTags(listOf("e", bad), listOf("p", recipientHex)).zappedNoteId(), bad)
}
}
@Test