跳至主要內容
高級

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 協議之上。若要深入了解, 可以參考 閃電網路技術文檔

已複製連結
已複製到剪貼簿