refactor(quartz): dedicated NutzapEvent.buildToUser for profile nutzaps

Restores the non-null zappedEvent on NutzapEvent.build and adds a
separate buildToUser builder (p tag only, no e/k tags) for nutzaps that
target a profile instead of an event — mirroring NIP-57's profile zap
convention. CashuWalletOps.sendNutzap dispatches between the two.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
This commit is contained in:
Claude
2026-06-11 19:48:56 +00:00
parent f2702f9299
commit 7d7f2f275f
2 changed files with 46 additions and 20 deletions
@@ -602,9 +602,10 @@ class CashuWalletOps(
*
* Spends [available] proofs at [mintUrl] to produce P2PK-locked outputs
* worth [amountSats] for [recipientPubKey]'s [recipientP2pkPubkeyHex].
* Publishes a kind:9321 with the locked proofs + message, rolls leftover
* change into a new kind:7375, NIP-09-deletes the source token events,
* and records the spend in kind:7376.
* Publishes a kind:9321 with the locked proofs + message (referencing
* [zappedEvent], or p-tag-only when null for a profile nutzap), rolls
* leftover change into a new kind:7375, NIP-09-deletes the source token
* events, and records the spend in kind:7376.
*
* Throws if `available` doesn't sum to at least [amountSats].
*/
@@ -637,14 +638,24 @@ class CashuWalletOps(
// Build the kind:9321 first so we have its id to reference from history.
val proofJsons = swap.send.map { nutzapProofJson.encodeToString(NutzapProofJson.serializer(), it.toNutzapJson()) }
val nutzapTemplate =
NutzapEvent.build(
message = message,
proofs = proofJsons,
mintUrl = mintUrl,
unit = "sat",
zappedEvent = zappedEvent,
recipientPubKey = recipientPubKey,
)
if (zappedEvent != null) {
NutzapEvent.build(
message = message,
proofs = proofJsons,
mintUrl = mintUrl,
unit = "sat",
zappedEvent = zappedEvent,
recipientPubKey = recipientPubKey,
)
} else {
NutzapEvent.buildToUser(
message = message,
proofs = proofJsons,
mintUrl = mintUrl,
unit = "sat",
recipientPubKey = recipientPubKey,
)
}
val nutzapEvent = signer.sign(nutzapTemplate)
publish(nutzapEvent)
// kind:9321 is out — recipient's auto-redeem can fire from here.
@@ -62,16 +62,35 @@ class NutzapEvent(
const val KIND = 9321
const val ALT_DESCRIPTION = "Nutzap"
/**
* [zappedEvent] is optional: a nutzap aimed at a profile rather than a
* specific event carries only the `p` tag (the `e`/`k` tags are omitted).
*/
fun build(
message: String,
proofs: List<String>,
mintUrl: String,
unit: String,
zappedEvent: EventHintBundle<out Event>?,
zappedEvent: EventHintBundle<out Event>,
recipientPubKey: HexKey,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<NutzapEvent>.() -> Unit = {},
) = eventTemplate(KIND, message, createdAt) {
alt(ALT_DESCRIPTION)
proofs(proofs)
mintUrl(mintUrl)
unit(unit)
zappedEvent(zappedEvent)
zappedEventKind(zappedEvent.event.kind)
recipient(recipientPubKey)
initializer()
}
/**
* Nutzap aimed at a profile rather than a specific event: carries only
* the `p` tag (no `e`/`k` tags), mirroring NIP-57's profile zaps.
*/
fun buildToUser(
message: String,
proofs: List<String>,
mintUrl: String,
unit: String,
recipientPubKey: HexKey,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<NutzapEvent>.() -> Unit = {},
@@ -80,10 +99,6 @@ class NutzapEvent(
proofs(proofs)
mintUrl(mintUrl)
unit(unit)
if (zappedEvent != null) {
zappedEvent(zappedEvent)
zappedEventKind(zappedEvent.event.kind)
}
recipient(recipientPubKey)
initializer()
}