NIP-61: Nutzaps
使用 Cashu ecash 進行 Zaps 打賞
概述
NIP-61 定義了 Nutzaps - 使用 Cashu ecash 代幣進行打賞的機制。 與傳統的閃電網路 Zaps(NIP-57)不同,Nutzaps 提供更強的隱私性, 因為 Cashu 使用盲簽名技術,造幣廠無法追蹤代幣流向。
Nutzaps 優勢
- 隱私性:無法追蹤支付來源
- 無需 LNURL:不依賴閃電網路服務
- 即時確認:無需等待閃電網路確認
- 低手續費:Cashu 交易通常免費
事件類型
| Kind | 說明 | 類型 |
|---|---|---|
9321 | Nutzap 事件 | 一般事件 |
10019 | Nutzap 資訊 | 可替換事件 |
Nutzap 資訊 (Kind 10019)
用戶發布此事件來宣告他們接受 Nutzaps 以及偏好的造幣廠。
標籤
| 標籤 | 說明 | 範例 |
|---|---|---|
mint | 接受的造幣廠 URL | ["mint", "https://mint.example.com", "sat"] |
relay | 接收 Nutzaps 的中繼器 | ["relay", "wss://relay.example.com"] |
pubkey | P2PK 鎖定公鑰 | ["pubkey", "hex公鑰"] |
範例
{
"kind": 10019,
"pubkey": "接收者公鑰",
"tags": [
["mint", "https://mint.minibits.cash", "sat"],
["mint", "https://mint.coinos.io", "sat"],
["relay", "wss://relay.damus.io"],
["relay", "wss://nos.lol"],
["pubkey", "用於 P2PK 鎖定的公鑰"]
],
"content": "",
"created_at": 1700000000
} Nutzap 事件 (Kind 9321)
實際的打賞事件,包含 Cashu 代幣。
標籤
| 標籤 | 說明 | 必要 |
|---|---|---|
amount | 金額(最小單位) | 是 |
unit | 單位(sat, msat, usd) | 是 |
proof | Cashu proof JSON | 是 |
u | 造幣廠 URL | 是 |
e | 被打賞的事件 ID | 否 |
p | 被打賞者公鑰 | 是 |
範例
{
"kind": 9321,
"pubkey": "打賞者公鑰",
"tags": [
["amount", "100"],
["unit", "sat"],
["u", "https://mint.minibits.cash"],
["proof", "{\"amount\":64,\"id\":\"009a1f293253e41e\",\"secret\":\"...\",\"C\":\"...\"}"],
["proof", "{\"amount\":32,\"id\":\"009a1f293253e41e\",\"secret\":\"...\",\"C\":\"...\"}"],
["proof", "{\"amount\":4,\"id\":\"009a1f293253e41e\",\"secret\":\"...\",\"C\":\"...\"}"],
["e", "被打賞的事件ID", "wss://relay.example.com"],
["p", "被打賞者公鑰"]
],
"content": "讚!很棒的內容 🎉",
"created_at": 1700000000
} P2PK 鎖定
為了確保只有接收者能夠使用代幣,可以使用 P2PK(Pay to Public Key)鎖定:
- 打賞者使用接收者的公鑰鎖定 Cashu 代幣
- 只有擁有對應私鑰的人才能解鎖並使用代幣
- 即使代幣被截獲,也無法被盜用
TypeScript 實作
發布 Nutzap 資訊
import { finalizeEvent } from 'nostr-tools';
interface NutzapConfig {
mints: { url: string; unit: string }[];
relays: string[];
p2pkPubkey?: string;
}
function createNutzapInfo(
config: NutzapConfig,
secretKey: Uint8Array
) {
const tags: string[][] = [];
config.mints.forEach((mint) => {
tags.push(['mint', mint.url, mint.unit]);
});
config.relays.forEach((relay) => {
tags.push(['relay', relay]);
});
if (config.p2pkPubkey) {
tags.push(['pubkey', config.p2pkPubkey]);
}
return finalizeEvent(
{
kind: 10019,
content: '',
tags,
created_at: Math.floor(Date.now() / 1000),
},
secretKey
);
}
// 使用範例
const nutzapInfo = createNutzapInfo(
{
mints: [
{ url: 'https://mint.minibits.cash', unit: 'sat' },
{ url: 'https://mint.coinos.io', unit: 'sat' },
],
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
p2pkPubkey: 'hex公鑰',
},
secretKey
); 發送 Nutzap
import { CashuMint, CashuWallet, getEncodedToken, Proof } from '@cashu/cashu-ts';
interface NutzapTarget {
pubkey: string;
eventId?: string;
relayHint?: string;
}
async function sendNutzap(
mintUrl: string,
proofs: Proof[],
amount: number,
target: NutzapTarget,
message: string,
secretKey: Uint8Array
) {
const mint = new CashuMint(mintUrl);
const wallet = new CashuWallet(mint);
// 選擇並拆分代幣
const { send } = await wallet.send(amount, proofs);
// 建立 Nutzap 事件
const tags: string[][] = [
['amount', amount.toString()],
['unit', 'sat'],
['u', mintUrl],
['p', target.pubkey],
];
// 添加每個 proof 作為單獨的標籤
send.forEach((proof) => {
tags.push(['proof', JSON.stringify(proof)]);
});
if (target.eventId) {
const eventTag = ['e', target.eventId];
if (target.relayHint) {
eventTag.push(target.relayHint);
}
tags.push(eventTag);
}
return finalizeEvent(
{
kind: 9321,
content: message,
tags,
created_at: Math.floor(Date.now() / 1000),
},
secretKey
);
}
// 使用範例
const nutzap = await sendNutzap(
'https://mint.minibits.cash',
myProofs,
100,
{
pubkey: '被打賞者公鑰',
eventId: '事件ID',
relayHint: 'wss://relay.example.com',
},
'很棒的內容!',
secretKey
); 接收 Nutzaps
import { SimplePool } from 'nostr-tools';
interface ReceivedNutzap {
id: string;
from: string;
amount: number;
unit: string;
mint: string;
proofs: Proof[];
message: string;
eventId?: string;
createdAt: number;
}
async function getReceivedNutzaps(
pool: SimplePool,
relays: string[],
pubkey: string,
since?: number
): Promise<ReceivedNutzap[]> {
const filter: any = {
kinds: [9321],
'#p': [pubkey],
};
if (since) {
filter.since = since;
}
const events = await pool.querySync(relays, filter);
return events.map((event) => {
const getTag = (name: string) =>
event.tags.find((t: string[]) => t[0] === name)?.[1];
const proofs = event.tags
.filter((t: string[]) => t[0] === 'proof')
.map((t: string[]) => JSON.parse(t[1]));
return {
id: event.id,
from: event.pubkey,
amount: parseInt(getTag('amount') || '0'),
unit: getTag('unit') || 'sat',
mint: getTag('u') || '',
proofs,
message: event.content,
eventId: getTag('e'),
createdAt: event.created_at,
};
});
}
// 兌換收到的 Nutzaps
async function redeemNutzap(
nutzap: ReceivedNutzap,
secretKey: Uint8Array
): Promise<Proof[]> {
const mint = new CashuMint(nutzap.mint);
const wallet = new CashuWallet(mint);
// 如果是 P2PK 鎖定的,需要提供私鑰
const newProofs = await wallet.receive(nutzap.proofs, {
privkey: bytesToHex(secretKey),
});
return newProofs;
} 查詢用戶的 Nutzap 配置
interface UserNutzapConfig {
mints: { url: string; unit: string }[];
relays: string[];
p2pkPubkey?: string;
}
async function getUserNutzapConfig(
pool: SimplePool,
relays: string[],
pubkey: string
): Promise<UserNutzapConfig | null> {
const events = await pool.querySync(relays, {
kinds: [10019],
authors: [pubkey],
});
if (events.length === 0) {
return null;
}
// 取最新的
const event = events.sort((a, b) => b.created_at - a.created_at)[0];
const mints = event.tags
.filter((t: string[]) => t[0] === 'mint')
.map((t: string[]) => ({ url: t[1], unit: t[2] || 'sat' }));
const configRelays = event.tags
.filter((t: string[]) => t[0] === 'relay')
.map((t: string[]) => t[1]);
const pubkeyTag = event.tags.find((t: string[]) => t[0] === 'pubkey');
return {
mints,
relays: configRelays,
p2pkPubkey: pubkeyTag?.[1],
};
}
// 檢查用戶是否支援 Nutzaps
async function supportsNutzaps(
pool: SimplePool,
relays: string[],
pubkey: string
): Promise<boolean> {
const config = await getUserNutzapConfig(pool, relays, pubkey);
return config !== null && config.mints.length > 0;
} Nutzaps vs 傳統 Zaps
| 特性 | Zaps (NIP-57) | Nutzaps (NIP-61) |
|---|---|---|
| 支付網路 | 閃電網路 | Cashu ecash |
| 隱私性 | 可追蹤 | 不可追蹤 |
| 設置要求 | LNURL 服務 | 僅需發布 kind 10019 |
| 確認時間 | 需等待閃電網路 | 即時 |
| 託管風險 | 無(非託管) | 有(造幣廠託管) |
| 互操作性 | 需 LNURL 支援 | 只需 Cashu 支援 |
安全考量
- 造幣廠信任:Cashu 是託管式的,選擇可信的造幣廠
- P2PK 鎖定:建議使用 P2PK 防止代幣被截獲
- 及時兌換:收到 Nutzap 後應及時兌換,防止造幣廠倒閉
- 多造幣廠:分散使用多個造幣廠降低風險
相關 NIPs
參考資源
已複製連結