高級
NIP-57 Zaps
深入了解 Nostr 的 Zaps 功能,透過閃電網路發送比特幣打賞並在 Nostr 上公開記錄。
12 分鐘
什麼是 Zaps?
Zaps 是 Nostr 上的公開閃電網路支付功能。當你「Zap」某人或某篇貼文時, 會透過閃電網路發送比特幣,並在 Nostr 上產生一個公開的收據(kind 9735 事件), 讓所有人都能看到這筆打賞。這創造了一個透明的價值交換系統。
前置要求: Zaps 需要收款方設定支援 NIP-57 的閃電網路地址(Lightning Address), 以及發送方需要有閃電網路錢包。
運作流程
1. 發送方請求 Zap
└─> 客戶端從收款方的 kind 0 取得 lud16(Lightning Address)
2. 取得發票
└─> 向 LNURL 伺服器發送 Zap 請求(含 nostr 參數)
└─> 伺服器回傳 BOLT11 發票
3. 支付發票
└─> 發送方用閃電錢包支付
4. 產生 Zap 收據
└─> LNURL 伺服器收到款項後
└─> 發布 kind 9735 事件到 Nostr
5. 客戶端顯示
└─> 訂閱者看到 Zap 收據並顯示 事件類型
Zap 請求(kind 9734)
由發送方建立,嵌入在 LNURL 請求中:
{
"kind": 9734,
"content": "Great post! ⚡", // 可選的 Zap 訊息
"tags": [
["p", "<收款方 pubkey>"],
["e", "<被 Zap 的事件 id>"], // 可選
["amount", "21000"], // 金額(毫聰)
["relays", "wss://relay1.com", "wss://relay2.com"],
["lnurl", "lnurl1..."] // 原始 LNURL
],
"pubkey": "<發送方 pubkey>",
"created_at": 1234567890,
"id": "...",
"sig": "..."
} Zap 收據(kind 9735)
由 LNURL 伺服器在收到付款後發布:
{
"kind": 9735,
"content": "",
"tags": [
["p", "<收款方 pubkey>"],
["P", "<發送方 pubkey>"], // 大寫 P
["e", "<被 Zap 的事件 id>"], // 可選
["bolt11", "lnbc210n1..."], // BOLT11 發票
["description", "{...}"], // 原始 zap request JSON
["preimage", "..."] // 付款 preimage
],
"pubkey": "<LNURL 伺服器 pubkey>", // 注意:非發送方
"created_at": 1234567890,
"id": "...",
"sig": "..."
} 設定接收 Zaps
1. 設定 Lightning Address
// 在 kind 0 (metadata) 中設定
{
"kind": 0,
"content": JSON.stringify({
"name": "Alice",
"lud16": "[email protected]", // Lightning Address
// 或者使用 LNURL
"lud06": "lnurl1..."
}),
"tags": [],
...
} 2. LNURL 伺服器要求
LNURL 伺服器必須支援 NIP-57 才能正確處理 Zaps:
// LNURL-pay 回應必須包含
{
"callback": "https://...",
"minSendable": 1000,
"maxSendable": 100000000,
"nostrPubkey": "<伺服器用於簽署 zap receipt 的 pubkey>",
"allowsNostr": true // 關鍵!表示支援 NIP-57
} 程式碼範例
發送 Zap
import { nip57 } from 'nostr-tools'
async function sendZap({
recipientPubkey,
eventId, // 可選:Zap 的事件
amount, // 毫聰
comment, // Zap 訊息
relays,
lightningAddress // 如 [email protected]
}) {
// 1. 取得 LNURL 資訊
const lnurlData = await fetch(
`https://${lightningAddress.split('@')[1]}/.well-known/lnurlp/${lightningAddress.split('@')[0]}`
).then(r => r.json())
// 2. 檢查是否支援 Zaps
if (!lnurlData.allowsNostr) {
throw new Error('此 Lightning Address 不支援 Zaps')
}
// 3. 建立 Zap 請求
const zapRequest = nip57.makeZapRequest({
profile: recipientPubkey,
event: eventId,
amount: amount,
comment: comment,
relays: relays
})
// 4. 簽署 Zap 請求
const signedZapRequest = await window.nostr.signEvent(zapRequest)
// 5. 請求發票
const callbackUrl = new URL(lnurlData.callback)
callbackUrl.searchParams.set('amount', amount.toString())
callbackUrl.searchParams.set('nostr', JSON.stringify(signedZapRequest))
const invoiceResponse = await fetch(callbackUrl).then(r => r.json())
// 6. 支付發票(使用 WebLN 或其他方式)
if (window.webln) {
await window.webln.sendPayment(invoiceResponse.pr)
console.log('Zap 發送成功!')
} else {
// 返回發票讓用戶手動支付
return invoiceResponse.pr
}
} 驗證 Zap 收據
import { nip57, verifyEvent } from 'nostr-tools'
function validateZapReceipt(zapReceipt, expectedLnurlPubkey) {
// 1. 驗證簽名
if (!verifyEvent(zapReceipt)) {
return { valid: false, error: '簽名無效' }
}
// 2. 驗證發布者是 LNURL 伺服器
if (zapReceipt.pubkey !== expectedLnurlPubkey) {
return { valid: false, error: 'pubkey 不匹配' }
}
// 3. 解析 description(原始 zap request)
const descriptionTag = zapReceipt.tags.find(t => t[0] === 'description')
if (!descriptionTag) {
return { valid: false, error: '缺少 description' }
}
const zapRequest = JSON.parse(descriptionTag[1])
// 4. 驗證 zap request 簽名
if (!verifyEvent(zapRequest)) {
return { valid: false, error: 'zap request 簽名無效' }
}
// 5. 驗證金額匹配
const bolt11Tag = zapReceipt.tags.find(t => t[0] === 'bolt11')
const amountTag = zapRequest.tags.find(t => t[0] === 'amount')
// 解析 bolt11 並比對金額...
return {
valid: true,
sender: zapRequest.pubkey,
recipient: zapReceipt.tags.find(t => t[0] === 'p')?.[1],
amount: amountTag?.[1],
comment: zapRequest.content
}
} 訂閱 Zaps
// 訂閱收到的 Zaps
function subscribeToMyZaps(relay, myPubkey) {
return relay.subscribe([{
kinds: [9735],
'#p': [myPubkey], // 我是收款方
since: Math.floor(Date.now() / 1000) - 86400 // 過去 24 小時
}], {
onevent(event) {
const descTag = event.tags.find(t => t[0] === 'description')
const zapRequest = JSON.parse(descTag[1])
console.log(`收到 Zap!`)
console.log(`發送者: ${zapRequest.pubkey}`)
console.log(`訊息: ${zapRequest.content}`)
}
})
}
// 訂閱特定貼文的 Zaps
function subscribeToPostZaps(relay, eventId) {
return relay.subscribe([{
kinds: [9735],
'#e': [eventId]
}], {
onevent(event) {
console.log('此貼文收到一個 Zap')
}
})
}
// 計算總 Zap 金額
async function getTotalZaps(relay, eventId) {
const zaps = await new Promise(resolve => {
const results = []
const sub = relay.subscribe([{
kinds: [9735],
'#e': [eventId]
}])
sub.on('event', e => results.push(e))
sub.on('eose', () => {
sub.close()
resolve(results)
})
})
let total = 0
for (const zap of zaps) {
const descTag = zap.tags.find(t => t[0] === 'description')
const zapRequest = JSON.parse(descTag[1])
const amountTag = zapRequest.tags.find(t => t[0] === 'amount')
if (amountTag) {
total += parseInt(amountTag[1])
}
}
return total // 毫聰
} 支援 Zaps 的服務
Alby
瀏覽器錢包
支援 NIP-57 的 Lightning Address
Wallet of Satoshi
行動錢包
支援 Zaps 的 LN Address
ZEBEDEE
遊戲錢包
Lightning Address 服務
Stacker News
社群
整合 Nostr Zaps
Primal
客戶端
內建 Zap 功能
Damus
客戶端
iOS Zaps 支援
匿名 Zaps
發送方可以選擇不公開身份:
// 匿名 Zap:使用一次性密鑰對簽署 zap request
import { generateSecretKey, getPublicKey } from 'nostr-tools'
function createAnonymousZapRequest(params) {
// 產生一次性密鑰對
const anonPrivkey = generateSecretKey()
const anonPubkey = getPublicKey(anonPrivkey)
const zapRequest = {
kind: 9734,
pubkey: anonPubkey, // 使用一次性公鑰
content: params.comment || '',
tags: [
['p', params.recipientPubkey],
['amount', params.amount.toString()],
['relays', ...params.relays],
['anon', ''] // 標記為匿名
],
created_at: Math.floor(Date.now() / 1000)
}
// 用一次性私鑰簽署
return signEvent(zapRequest, anonPrivkey)
}
// 檢查 Zap 是否匿名
function isAnonymousZap(zapReceipt) {
const descTag = zapReceipt.tags.find(t => t[0] === 'description')
const zapRequest = JSON.parse(descTag[1])
return zapRequest.tags.some(t => t[0] === 'anon')
} 金額格式
| 單位 | 換算 | NIP-57 中 |
|---|---|---|
| 毫聰 (msat) | 1 sat = 1000 msat | amount 標籤使用 |
| 聰 (sat) | 1 BTC = 100,000,000 sat | 常見顯示單位 |
| 21 sats | = 21,000 msat | 常見 Zap 金額 |
常見問題
為什麼 Zap 沒有顯示?
- • LNURL 伺服器可能不支援 NIP-57
- • 收據可能發布到不同的中繼器
- • 等待區塊確認中
Zap 可以退款嗎?
- • 閃電網路付款不可逆
- • 收款方可以選擇退還
- • 發送前請確認金額
延伸閱讀: Zaps 建立在 LNURL-pay 協議之上。若要深入了解, 可以參考 閃電網路技術文檔。
已複製連結