feat: clarify zap receipts and add kind 9735 anatomy slide
Rework the zap slide's step 2 to say the recipient's Lightning provider signs the receipt with its OWN nostr key (advertised as nostrPubkey), and add a dedicated 'Anatomy of a zap receipt' slide breaking down the kind 9735 event: who signs it (service), who was zapped (p tag), and who zapped (the embedded signed 9734 in description). Grounded in NIP-57 and the lnbits/lnurlp implementation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5f97b48f6d
commit
abbfcaf3ef
3 changed files with 88 additions and 7 deletions
72
src/components/slides/ZapReceiptSlide.vue
Normal file
72
src/components/slides/ZapReceiptSlide.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<script setup lang="ts">
|
||||
import SlideShell from './SlideShell.vue'
|
||||
|
||||
type Role = 'server' | 'recipient' | 'sender'
|
||||
interface Line {
|
||||
key: string
|
||||
value: string
|
||||
kind: 'string' | 'number'
|
||||
comment: string
|
||||
role?: Role
|
||||
tag?: boolean // an entry in the event's `tags` array (vs. a top-level field)
|
||||
}
|
||||
|
||||
const lines: Line[] = [
|
||||
{ key: 'kind', value: '9735', kind: 'number', comment: 'a zap receipt' },
|
||||
{ key: 'pubkey', value: '"9be0…7c"', kind: 'string', comment: 'the zapper service’s key — it signs, not you', role: 'server' },
|
||||
{ key: 'p', value: '"3bf0c6…2a"', kind: 'string', comment: 'who was zapped (recipient)', role: 'recipient', tag: true },
|
||||
{ key: 'e', value: '"0a1b2c…9f"', kind: 'string', comment: 'which note (optional)', tag: true },
|
||||
{ key: 'P', value: '"82f4…d1"', kind: 'string', comment: 'who zapped (optional)', role: 'sender', tag: true },
|
||||
{ key: 'bolt11', value: '"lnbc210n1…"', kind: 'string', comment: 'the invoice that was paid', tag: true },
|
||||
{ key: 'description', value: '"{…9734…}"', kind: 'string', comment: 'your signed request: sender + amount + comment', role: 'sender', tag: true },
|
||||
{ key: 'sig', value: '"e7c9…"', kind: 'string', comment: 'signed by the service key', role: 'server' },
|
||||
]
|
||||
|
||||
const valueClass = { string: 'text-emerald-300', number: 'text-amber-300' }
|
||||
const roleBadge: Record<Role, { label: string; class: string }> = {
|
||||
server: { label: 'server', class: 'bg-violet-500/15 text-violet-300' },
|
||||
recipient: { label: 'zapped', class: 'bg-sky-500/15 text-sky-300' },
|
||||
sender: { label: 'zapper', class: 'bg-rose-500/15 text-rose-300' },
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SlideShell kicker="NIP-57 · kind 9735" title="Anatomy of a zap receipt">
|
||||
<p class="mb-4 text-zinc-400">The event the provider publishes when your zap is paid:</p>
|
||||
|
||||
<div class="flex items-center justify-center gap-3 sm:gap-6">
|
||||
<span class="select-none text-[8rem] font-thin leading-none text-violet-500/80 sm:text-[10rem]">{</span>
|
||||
|
||||
<div class="grid grid-cols-[auto_auto_1fr] items-center gap-x-5 gap-y-1.5 font-mono text-base sm:text-lg">
|
||||
<template v-for="l in lines" :key="l.key">
|
||||
<span class="justify-self-end whitespace-nowrap" :class="l.tag ? 'text-violet-300/90' : 'font-semibold text-violet-300'">
|
||||
<span v-if="l.tag" class="text-zinc-600">tag </span>"{{ l.key }}"<span class="text-zinc-500">:</span>
|
||||
</span>
|
||||
<span class="whitespace-nowrap" :class="valueClass[l.kind]">{{ l.value }}</span>
|
||||
<span class="whitespace-nowrap text-sm text-zinc-600">
|
||||
<span class="text-zinc-500">// </span>{{ l.comment
|
||||
}}<span
|
||||
v-if="l.role"
|
||||
class="ml-1 rounded px-1 text-xs font-semibold"
|
||||
:class="roleBadge[l.role].class"
|
||||
>{{ roleBadge[l.role].label }}</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<span class="select-none text-[8rem] font-thin leading-none text-violet-500/80 sm:text-[10rem]">}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap justify-center gap-x-6 gap-y-1 text-sm">
|
||||
<span><span class="mr-1 inline-block h-2 w-2 rounded-full bg-violet-400" />service signs it</span>
|
||||
<span><span class="mr-1 inline-block h-2 w-2 rounded-full bg-sky-400" /><code>p</code> = who was zapped</span>
|
||||
<span><span class="mr-1 inline-block h-2 w-2 rounded-full bg-rose-400" /><code>description</code> = who zapped (your signed 9734)</span>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-center text-sm text-zinc-500">
|
||||
Clients trust it only if <code class="text-zinc-300">pubkey</code> matches the
|
||||
<code class="text-zinc-300">nostrPubkey</code> the recipient’s provider advertised — that’s what stops fake zaps.
|
||||
</p>
|
||||
</SlideShell>
|
||||
</template>
|
||||
|
|
@ -29,10 +29,12 @@ import SlideShell from './SlideShell.vue'
|
|||
<BadgeCheck class="h-6 w-6 text-violet-400" />
|
||||
<span class="text-sm font-semibold uppercase tracking-wider text-violet-400">The receipt · the Nostr part</span>
|
||||
</div>
|
||||
<p class="text-white">Their Lightning server publishes a signed <span class="font-semibold">zap receipt</span>.</p>
|
||||
<p class="text-white">Their Lightning provider signs a <span class="font-semibold">zap receipt</span>.</p>
|
||||
<p class="mt-2 text-base text-zinc-400">
|
||||
Once the invoice clears, the server signs a receipt proving payment and posts it to relays.
|
||||
Now the zap shows up <span class="text-zinc-200">publicly under the note</span> — amount, sender, comment.
|
||||
The provider has its <span class="text-zinc-200">own Nostr key</span>. When the invoice clears it
|
||||
signs a <code class="text-zinc-300">kind 9735</code> event — tagged with
|
||||
<span class="text-zinc-200">who was zapped</span> and embedding your signed request (which says who
|
||||
zapped, how much, and any comment) — then posts it to the relays you named.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -47,10 +49,11 @@ import SlideShell from './SlideShell.vue'
|
|||
</div>
|
||||
|
||||
<p class="mt-4 text-sm text-zinc-500">
|
||||
Under the hood (NIP-57): your client attaches a signed
|
||||
<code class="text-zinc-300">zap request (kind 9734)</code>; the recipient’s server returns a
|
||||
<code class="text-zinc-300">zap receipt (kind 9735)</code>. The server must opt in via
|
||||
<code class="text-zinc-300">allowsNostr</code>.
|
||||
Under the hood (NIP-57): your signed
|
||||
<code class="text-zinc-300">zap request (kind 9734)</code> rides along with the LNURL-pay invoice; the
|
||||
provider advertises <code class="text-zinc-300">allowsNostr</code> + its
|
||||
<code class="text-zinc-300">nostrPubkey</code>, and clients only trust a
|
||||
<code class="text-zinc-300">9735</code> receipt signed by that key. (Next: its anatomy →)
|
||||
</p>
|
||||
</SlideShell>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import OutboxSlide from '@/components/slides/OutboxSlide.vue'
|
|||
import RelayCategoriesSlide from '@/components/slides/RelayCategoriesSlide.vue'
|
||||
import RelayAccessSlide from '@/components/slides/RelayAccessSlide.vue'
|
||||
import ZapSlide from '@/components/slides/ZapSlide.vue'
|
||||
import ZapReceiptSlide from '@/components/slides/ZapReceiptSlide.vue'
|
||||
import ResourcesSlide from '@/components/slides/ResourcesSlide.vue'
|
||||
|
||||
/**
|
||||
|
|
@ -205,6 +206,11 @@ export const slides: Slide[] = [
|
|||
label: 'How a zap works',
|
||||
component: ZapSlide,
|
||||
},
|
||||
{
|
||||
id: 'zap-receipt',
|
||||
label: 'Zap receipt anatomy',
|
||||
component: ZapReceiptSlide,
|
||||
},
|
||||
{
|
||||
id: 'recap',
|
||||
label: 'Recap',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue