跳至主要內容
高級

NIP-47 錢包連接

深入了解 Nostr Wallet Connect (NWC),讓應用程式透過 Nostr 協議控制閃電網路錢包。

12 分鐘

什麼是 NIP-47?

NIP-47 定義了 Nostr Wallet Connect (NWC) 協議,讓應用程式可以透過 Nostr 中繼器與閃電網路錢包通訊。應用程式可以請求付款、產生發票、查詢餘額等, 而用戶保持對錢包的完全控制權。

核心優勢: NWC 讓用戶可以將任何支援的錢包連接到任何支援的應用程式, 無需在每個應用中設定錢包。一次連接,處處使用。

運作原理

錢包 (Wallet Service)              應用程式 (Client)
        │                                    │
        │  1. 產生連接 URI                    │
        │     nostr+walletconnect://...      │
        │  ─────────────────────────────────>│
        │                                    │
        │  2. 掃描 QR Code 或輸入 URI         │
        │                                    │
        │  3. 發送加密請求 (kind 23194)       │
        │  <─────────────────────────────────│
        │                                    │
        │  4. 處理請求(付款、產生發票等)     │
        │                                    │
        │  5. 發送加密回應 (kind 23195)       │
        │  ─────────────────────────────────>│
        │                                    │

連接 URI 格式

nostr+walletconnect://<wallet-pubkey>?
  relay=wss://relay.example.com&
  secret=<connection-secret>&
  [email protected]

// 參數說明:
// wallet-pubkey: 錢包服務的公鑰
// relay: 用於通訊的中繼器(可多個)
// secret: 連接密鑰(用於加密通訊)
// lud16: 可選的 Lightning Address

// 實際範例
nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.getalby.com%2Fv1&secret=12345abcde

事件類型

Kind 名稱 發送方 說明
23194 Request 應用程式 加密的 RPC 請求
23195 Response 錢包 加密的 RPC 回應
13194 Info 錢包 錢包資訊(支援的方法)

支援的方法

付款相關

  • pay_invoice
  • pay_keysend
  • multi_pay_invoice
  • multi_pay_keysend

發票相關

  • make_invoice
  • lookup_invoice
  • list_transactions

資訊查詢

  • get_balance
  • get_info

進階功能

  • sign_message
  • notifications

請求格式

// 請求事件 (kind 23194)
{
  "kind": 23194,
  "content": "<NIP-04 加密的 JSON>",
  "tags": [
    ["p", "<錢包公鑰>"]
  ],
  "pubkey": "<應用程式公鑰>",
  ...
}

// 解密後的 content 格式
{
  "method": "pay_invoice",
  "params": {
    "invoice": "lnbc100n1p..."
  }
}

// pay_invoice 請求
{
  "method": "pay_invoice",
  "params": {
    "invoice": "lnbc100n1p...",
    "amount": 1000  // 可選,覆蓋發票金額(毫聰)
  }
}

// make_invoice 請求
{
  "method": "make_invoice",
  "params": {
    "amount": 21000,           // 毫聰
    "description": "Coffee",
    "description_hash": "...", // 可選
    "expiry": 3600             // 可選,秒
  }
}

// get_balance 請求
{
  "method": "get_balance",
  "params": {}
}

回應格式

// 回應事件 (kind 23195)
{
  "kind": 23195,
  "content": "<NIP-04 加密的 JSON>",
  "tags": [
    ["p", "<應用程式公鑰>"],
    ["e", "<請求事件 ID>"]
  ],
  "pubkey": "<錢包公鑰>",
  ...
}

// 成功回應
{
  "result_type": "pay_invoice",
  "result": {
    "preimage": "abcdef123456..."
  }
}

// make_invoice 成功回應
{
  "result_type": "make_invoice",
  "result": {
    "type": "incoming",
    "invoice": "lnbc100n1p...",
    "description": "Coffee",
    "description_hash": null,
    "preimage": null,
    "payment_hash": "abc123...",
    "amount": 21000,
    "fees_paid": 0,
    "created_at": 1704067200,
    "expires_at": 1704070800,
    "settled_at": null
  }
}

// get_balance 成功回應
{
  "result_type": "get_balance",
  "result": {
    "balance": 100000000  // 毫聰
  }
}

// 錯誤回應
{
  "result_type": "pay_invoice",
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "餘額不足"
  }
}

程式碼範例

解析連接 URI

function parseNwcUri(uri) {
  // 移除前綴
  const withoutPrefix = uri.replace('nostr+walletconnect://', '')

  // 分離公鑰和參數
  const [pubkey, queryString] = withoutPrefix.split('?')

  // 解析參數
  const params = new URLSearchParams(queryString)

  return {
    walletPubkey: pubkey,
    relays: params.getAll('relay').map(decodeURIComponent),
    secret: params.get('secret'),
    lud16: params.get('lud16')
  }
}

// 使用
const connection = parseNwcUri(
  'nostr+walletconnect://abc123...?relay=wss%3A%2F%2Frelay.example.com&secret=xyz'
)
console.log(connection.walletPubkey)
console.log(connection.relays)
console.log(connection.secret)

NWC 客戶端類別

import { nip04, generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools'

class NwcClient {
  constructor(connectionUri) {
    const parsed = parseNwcUri(connectionUri)
    this.walletPubkey = parsed.walletPubkey
    this.relays = parsed.relays
    this.secret = parsed.secret

    // 從 secret 派生密鑰對
    this.privateKey = this.derivePrivateKey(parsed.secret)
    this.publicKey = getPublicKey(this.privateKey)
  }

  derivePrivateKey(secret) {
    // 使用 secret 作為種子派生私鑰
    const encoder = new TextEncoder()
    const data = encoder.encode(secret)
    // 簡化示例,實際應使用 HKDF
    return crypto.subtle.digest('SHA-256', data)
  }

  async sendRequest(method, params) {
    // 1. 建立請求內容
    const content = JSON.stringify({ method, params })

    // 2. 加密內容(NIP-04)
    const encrypted = await nip04.encrypt(
      this.privateKey,
      this.walletPubkey,
      content
    )

    // 3. 建立事件
    const event = finalizeEvent({
      kind: 23194,
      content: encrypted,
      tags: [['p', this.walletPubkey]],
      created_at: Math.floor(Date.now() / 1000)
    }, this.privateKey)

    // 4. 發送到中繼器
    const relay = await this.connectRelay()
    await relay.publish(event)

    // 5. 等待回應
    return this.waitForResponse(event.id)
  }

  async waitForResponse(requestId) {
    return new Promise((resolve, reject) => {
      const relay = this.relay

      const sub = relay.subscribe([{
        kinds: [23195],
        '#e': [requestId],
        '#p': [this.publicKey]
      }])

      const timeout = setTimeout(() => {
        sub.close()
        reject(new Error('請求逾時'))
      }, 30000)

      sub.on('event', async (event) => {
        clearTimeout(timeout)
        sub.close()

        // 解密回應
        const decrypted = await nip04.decrypt(
          this.privateKey,
          this.walletPubkey,
          event.content
        )

        const response = JSON.parse(decrypted)

        if (response.error) {
          reject(new Error(response.error.message))
        } else {
          resolve(response.result)
        }
      })
    })
  }

  // 便捷方法
  async payInvoice(invoice, amount = null) {
    const params = { invoice }
    if (amount) params.amount = amount
    return this.sendRequest('pay_invoice', params)
  }

  async makeInvoice(amount, description = '') {
    return this.sendRequest('make_invoice', {
      amount,
      description
    })
  }

  async getBalance() {
    return this.sendRequest('get_balance', {})
  }

  async getInfo() {
    return this.sendRequest('get_info', {})
  }
}

// 使用
const nwc = new NwcClient('nostr+walletconnect://...')
const balance = await nwc.getBalance()
console.log(`餘額: ${balance.balance / 1000} sats`)

使用 @getalby/sdk

import { nwc } from '@getalby/sdk'

// 使用現成的 SDK 更簡單
const client = new nwc.NWCClient({
  nostrWalletConnectUrl: 'nostr+walletconnect://...'
})

// 付款
const response = await client.payInvoice({
  invoice: 'lnbc100n1p...'
})
console.log('付款成功,preimage:', response.preimage)

// 產生發票
const invoice = await client.makeInvoice({
  amount: 21000,  // 毫聰
  description: '咖啡'
})
console.log('發票:', invoice.invoice)

// 查詢餘額
const balance = await client.getBalance()
console.log('餘額:', balance.balance, '毫聰')

// 取得錢包資訊
const info = await client.getInfo()
console.log('支援的方法:', info.methods)

支援的錢包

Alby

瀏覽器擴充錢包

完整 NWC 支援

Mutiny Wallet

自託管網頁錢包

NWC 支援

Umbrel

家用節點

NWC 應用程式

Start9

個人伺服器

NWC 支援

LNbits

帳戶系統

NWC 擴充

Coinos

託管錢包

NWC 支援

權限控制

錢包可以為每個連接設定權限限制:

// 錢包資訊事件 (kind 13194)
{
  "kind": 13194,
  "content": "pay_invoice get_balance get_info make_invoice",
  "tags": [],
  "pubkey": "<錢包公鑰>"
}

// content 列出支援的方法
// 應用程式應該檢查這個列表

// 常見權限配置範例:
// 只讀:get_balance get_info list_transactions lookup_invoice
// 收款:get_balance make_invoice lookup_invoice
// 完整:pay_invoice make_invoice get_balance get_info ...

// 預算限制(由錢包實作)
// - 單筆付款上限
// - 每日/每月付款上限
// - 總付款上限

錯誤代碼

代碼 說明
RATE_LIMITED 請求過於頻繁
NOT_IMPLEMENTED 方法不支援
INSUFFICIENT_BALANCE 餘額不足
QUOTA_EXCEEDED 超出預算限制
RESTRICTED 權限不足
UNAUTHORIZED 未授權
INTERNAL 內部錯誤
PAYMENT_FAILED 付款失敗

安全考量

安全特性

  • • NIP-04 端對端加密
  • • 連接特定的密鑰對
  • • 可設定預算限制
  • • 可隨時撤銷連接

注意事項

  • • 謹慎分享連接 URI
  • • 設定合理的預算限制
  • • 定期檢查已連接的應用
  • • 不要在不信任的設備上使用

延伸閱讀: NWC 是閃電網路與 Nostr 整合的重要橋樑。搭配 NIP-57 Zaps, 可以實現完全去中心化的社交打賞系統。

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