feat(quartz): add NIP-BC onchain zaps (kind 8333)

Implements NIP-BC onchain zap events analogous to NIP-57 lightning
zap receipts but for Bitcoin transactions. The recipient's Nostr
pubkey is used directly as the internal key of a BIP-341 P2TR output.

Mirrors the nip88Polls package structure with a single `zap/` subpackage:
- OnchainZapEvent (kind 8333) with profile / event / addressable-event
  zap builders
- Tags: BitcoinTxIdTag (i = bitcoin:tx:<txid>), AmountTag (sats),
  BlockTag (hash + height), ProofTag (raw-tx + merkle proof) for
  optional SPV verification
- TagArrayExt / TagArrayBuilderExt for parsing and assembly
- EventFactory wired so kind 8333 deserializes into OnchainZapEvent

Reuses standard ETag/ATag/PTag/KindTag from nip01Core. Does not yet
implement client-side transaction verification or PSBT signer methods.
This commit is contained in:
Claude
2026-05-14 03:25:44 +00:00
parent 17f207bb31
commit d095dc790d
8 changed files with 473 additions and 0 deletions
@@ -0,0 +1,144 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nipBCOnchainZaps.zap
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.aTag.toATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.toETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
/**
* NIP-BC: Onchain Zaps.
*
* Kind 8333 event that attributes a Bitcoin onchain payment to a Nostr event or profile.
* The recipient's Nostr pubkey is used directly as the internal key of a BIP-341 P2TR
* output, so every Nostr pubkey has exactly one corresponding mainnet Taproot address.
*
* Mainnet only.
*/
@Immutable
class OnchainZapEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider {
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
override fun eventHints() = tags.mapNotNull(ETag::parseAsHint)
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId)
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
/** The Bitcoin transaction id (64-char lowercase hex) parsed from the `i` tag. */
fun txid() = tags.txid()
/** Sender-claimed amount in satoshis. Must be verified against the on-chain transaction. */
fun claimedAmountInSats() = tags.amountInSats()
/** The hex-encoded pubkey of the recipient (the author being paid). */
fun recipient() = tags.firstNotNullOfOrNull(PTag::parseKey)
/** The event being zapped, if any. */
fun zappedEvent() = tags.firstNotNullOfOrNull(ETag::parseId)
/** The addressable event being zapped, if any. */
fun zappedAddress() = tags.firstNotNullOfOrNull(ATag::parseAddressId)
/** Optional block tag with hash + height enabling SPV verification. */
fun block() = tags.block()
/** Optional inline SPV proof (raw tx hex + merkle proof hex). */
fun proof() = tags.proof()
/** True when neither `e` nor `a` is present — the zap targets the recipient's profile. */
fun isProfileZap() = zappedEvent() == null && zappedAddress() == null
companion object {
const val KIND = 8333
const val ALT_DESCRIPTION = "Onchain Zap"
/**
* Build an onchain zap that targets a specific event.
*/
fun build(
txid: String,
recipientPubKey: HexKey,
amountInSats: Long,
zappedEvent: EventHintBundle<out Event>,
content: String = "",
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<OnchainZapEvent>.() -> Unit = {},
) = eventTemplate(KIND, content, createdAt) {
alt(ALT_DESCRIPTION)
txid(txid)
recipient(recipientPubKey)
amountInSats(amountInSats)
if (zappedEvent.event is AddressableEvent) {
zappedAddress(zappedEvent.toATag())
}
zappedEvent(zappedEvent.toETag())
zappedKind(zappedEvent.event.kind)
initializer()
}
/**
* Build an onchain zap that targets a recipient's profile (no event / address).
*/
fun buildProfileZap(
txid: String,
recipientPubKey: HexKey,
amountInSats: Long,
content: String = "",
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<OnchainZapEvent>.() -> Unit = {},
) = eventTemplate(KIND, content, createdAt) {
alt(ALT_DESCRIPTION)
txid(txid)
recipient(recipientPubKey)
amountInSats(amountInSats)
initializer()
}
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nipBCOnchainZaps.zap
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.kinds.KindTag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags.AmountTag
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags.BitcoinTxIdTag
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags.BlockTag
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags.ProofTag
fun TagArrayBuilder<OnchainZapEvent>.txid(txid: String) = addUnique(BitcoinTxIdTag.assemble(txid))
fun TagArrayBuilder<OnchainZapEvent>.recipient(recipientPubKey: HexKey) = addUnique(PTag.assemble(recipientPubKey, null))
fun TagArrayBuilder<OnchainZapEvent>.amountInSats(amountInSats: Long) = addUnique(AmountTag.assemble(amountInSats))
fun TagArrayBuilder<OnchainZapEvent>.zappedEvent(tag: ETag) = addUnique(tag.toTagArray())
fun TagArrayBuilder<OnchainZapEvent>.zappedAddress(tag: ATag) = addUnique(tag.toATagArray())
fun TagArrayBuilder<OnchainZapEvent>.zappedKind(kind: Int) = addUnique(KindTag.assemble(kind))
fun TagArrayBuilder<OnchainZapEvent>.block(
blockHashHex: String,
height: Long,
) = addUnique(BlockTag.assemble(blockHashHex, height))
fun TagArrayBuilder<OnchainZapEvent>.block(block: BlockTag) = addUnique(block.toTagArray())
fun TagArrayBuilder<OnchainZapEvent>.proof(
rawTxHex: String,
merkleProofHex: String,
) = addUnique(ProofTag.assemble(rawTxHex, merkleProofHex))
fun TagArrayBuilder<OnchainZapEvent>.proof(proof: ProofTag) = addUnique(proof.toTagArray())
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nipBCOnchainZaps.zap
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags.AmountTag
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags.BitcoinTxIdTag
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags.BlockTag
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags.ProofTag
fun TagArray.txid() = firstNotNullOfOrNull(BitcoinTxIdTag::parse)
fun TagArray.amountInSats() = firstNotNullOfOrNull(AmountTag::parse)
fun TagArray.block() = firstNotNullOfOrNull(BlockTag::parse)
fun TagArray.proof() = firstNotNullOfOrNull(ProofTag::parse)
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
class AmountTag {
companion object {
const val TAG_NAME = "amount"
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
fun parse(tag: Array<String>): Long? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1].toLongOrNull()
}
fun assemble(amountInSats: Long) = arrayOf(TAG_NAME, amountInSats.toString())
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.ensure
class BitcoinTxIdTag {
companion object {
const val TAG_NAME = "i"
const val PREFIX = "bitcoin:tx:"
const val TXID_LENGTH = 64
fun isTag(tag: Array<String>) =
tag.has(1) &&
tag[0] == TAG_NAME &&
tag[1].startsWith(PREFIX) &&
tag[1].length == PREFIX.length + TXID_LENGTH
fun isTagged(
tag: Array<String>,
txid: String,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == assembleScope(txid)
fun parse(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].startsWith(PREFIX)) { return null }
val txid = tag[1].substring(PREFIX.length)
ensure(txid.length == TXID_LENGTH) { return null }
ensure(txid == txid.lowercase()) { return null }
ensure(Hex.isHex(txid)) { return null }
return txid
}
fun parseScope(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].startsWith(PREFIX)) { return null }
return tag[1]
}
fun assembleScope(txid: String) = PREFIX + txid.lowercase()
fun assemble(txid: String) = arrayOf(TAG_NAME, assembleScope(txid))
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.ensure
@Immutable
data class BlockTag(
val blockHashHex: String,
val height: Long,
) {
fun toTagArray() = assemble(blockHashHex, height)
companion object {
const val TAG_NAME = "block"
const val BLOCK_HASH_LENGTH = 64
fun isTag(tag: Array<String>) =
tag.has(2) &&
tag[0] == TAG_NAME &&
tag[1].length == BLOCK_HASH_LENGTH &&
tag[2].isNotEmpty()
fun parse(tag: Array<String>): BlockTag? {
ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == BLOCK_HASH_LENGTH) { return null }
ensure(Hex.isHex(tag[1])) { return null }
ensure(tag[2].isNotEmpty()) { return null }
val height = tag[2].toLongOrNull() ?: return null
ensure(height >= 0) { return null }
return BlockTag(tag[1].lowercase(), height)
}
fun assemble(
blockHashHex: String,
height: Long,
) = arrayOf(TAG_NAME, blockHashHex.lowercase(), height.toString())
fun assemble(block: BlockTag) = assemble(block.blockHashHex, block.height)
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nipBCOnchainZaps.zap.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.ensure
@Immutable
data class ProofTag(
val rawTxHex: String,
val merkleProofHex: String,
) {
fun toTagArray() = assemble(rawTxHex, merkleProofHex)
companion object {
const val TAG_NAME = "proof"
fun isTag(tag: Array<String>) =
tag.has(2) &&
tag[0] == TAG_NAME &&
tag[1].isNotEmpty()
fun parse(tag: Array<String>): ProofTag? {
ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
ensure(Hex.isHex(tag[1])) { return null }
ensure(Hex.isHex(tag[2])) { return null }
return ProofTag(tag[1].lowercase(), tag[2].lowercase())
}
fun assemble(
rawTxHex: String,
merkleProofHex: String,
) = arrayOf(TAG_NAME, rawTxHex.lowercase(), merkleProofHex.lowercase())
fun assemble(proof: ProofTag) = assemble(proof.rawTxHex, proof.merkleProofHex)
}
}
@@ -286,6 +286,7 @@ import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallRenegotiateEvent
import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent
import com.vitorpamplona.quartz.nipBCOnchainZaps.zap.OnchainZapEvent
import com.vitorpamplona.quartz.nipC0CodeSnippets.CodeSnippetEvent
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
@@ -517,6 +518,7 @@ class EventFactory {
NIP90EventPublishScheduleResponseEvent.KIND -> NIP90EventPublishScheduleResponseEvent(id, pubKey, createdAt, tags, content, sig)
NIP90EventPowDelegationRequestEvent.KIND -> NIP90EventPowDelegationRequestEvent(id, pubKey, createdAt, tags, content, sig)
NIP90EventPowDelegationResponseEvent.KIND -> NIP90EventPowDelegationResponseEvent(id, pubKey, createdAt, tags, content, sig)
OnchainZapEvent.KIND -> OnchainZapEvent(id, pubKey, createdAt, tags, content, sig)
OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig)
PaymentTargetsEvent.KIND -> PaymentTargetsEvent(id, pubKey, createdAt, tags, content, sig)
PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig)