From d095dc790d149df29ec35936266a44f42ae1a413 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 03:25:44 +0000 Subject: [PATCH] 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:), 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. --- .../nipBCOnchainZaps/zap/OnchainZapEvent.kt | 144 ++++++++++++++++++ .../zap/TagArrayBuilderExt.kt | 58 +++++++ .../nipBCOnchainZaps/zap/TagArrayExt.kt | 35 +++++ .../nipBCOnchainZaps/zap/tags/AmountTag.kt | 41 +++++ .../zap/tags/BitcoinTxIdTag.kt | 68 +++++++++ .../nipBCOnchainZaps/zap/tags/BlockTag.kt | 65 ++++++++ .../nipBCOnchainZaps/zap/tags/ProofTag.kt | 60 ++++++++ .../quartz/utils/EventFactory.kt | 2 + 8 files changed, 473 insertions(+) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/OnchainZapEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/TagArrayExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/AmountTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/BitcoinTxIdTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/BlockTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/ProofTag.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/OnchainZapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/OnchainZapEvent.kt new file mode 100644 index 000000000..63dc0cf27 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/OnchainZapEvent.kt @@ -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>, + 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, + content: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> 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.() -> Unit = {}, + ) = eventTemplate(KIND, content, createdAt) { + alt(ALT_DESCRIPTION) + txid(txid) + recipient(recipientPubKey) + amountInSats(amountInSats) + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/TagArrayBuilderExt.kt new file mode 100644 index 000000000..f07b56a51 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/TagArrayBuilderExt.kt @@ -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.txid(txid: String) = addUnique(BitcoinTxIdTag.assemble(txid)) + +fun TagArrayBuilder.recipient(recipientPubKey: HexKey) = addUnique(PTag.assemble(recipientPubKey, null)) + +fun TagArrayBuilder.amountInSats(amountInSats: Long) = addUnique(AmountTag.assemble(amountInSats)) + +fun TagArrayBuilder.zappedEvent(tag: ETag) = addUnique(tag.toTagArray()) + +fun TagArrayBuilder.zappedAddress(tag: ATag) = addUnique(tag.toATagArray()) + +fun TagArrayBuilder.zappedKind(kind: Int) = addUnique(KindTag.assemble(kind)) + +fun TagArrayBuilder.block( + blockHashHex: String, + height: Long, +) = addUnique(BlockTag.assemble(blockHashHex, height)) + +fun TagArrayBuilder.block(block: BlockTag) = addUnique(block.toTagArray()) + +fun TagArrayBuilder.proof( + rawTxHex: String, + merkleProofHex: String, +) = addUnique(ProofTag.assemble(rawTxHex, merkleProofHex)) + +fun TagArrayBuilder.proof(proof: ProofTag) = addUnique(proof.toTagArray()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/TagArrayExt.kt new file mode 100644 index 000000000..78c3a2efa --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/TagArrayExt.kt @@ -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) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/AmountTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/AmountTag.kt new file mode 100644 index 000000000..cff5db482 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/AmountTag.kt @@ -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) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): 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()) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/BitcoinTxIdTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/BitcoinTxIdTag.kt new file mode 100644 index 000000000..b4972fc32 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/BitcoinTxIdTag.kt @@ -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) = + tag.has(1) && + tag[0] == TAG_NAME && + tag[1].startsWith(PREFIX) && + tag[1].length == PREFIX.length + TXID_LENGTH + + fun isTagged( + tag: Array, + txid: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == assembleScope(txid) + + fun parse(tag: Array): 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? { + 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)) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/BlockTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/BlockTag.kt new file mode 100644 index 000000000..a2e5fec2a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/BlockTag.kt @@ -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) = + tag.has(2) && + tag[0] == TAG_NAME && + tag[1].length == BLOCK_HASH_LENGTH && + tag[2].isNotEmpty() + + fun parse(tag: Array): 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) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/ProofTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/ProofTag.kt new file mode 100644 index 000000000..8586fc883 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/zap/tags/ProofTag.kt @@ -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) = + tag.has(2) && + tag[0] == TAG_NAME && + tag[1].isNotEmpty() + + fun parse(tag: Array): 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) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index d3b80ffe7..993c41463 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -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)