NIP-60: Cashu Wallet
在 Nostr 上管理 Cashu ecash 錢包
概述
NIP-60 定義了在 Nostr 上儲存和管理 Cashu ecash 錢包的標準。 Cashu 是一種基於 Chaumian ecash 的隱私支付協議,與閃電網路整合。 通過 NIP-60,用戶可以在不同客戶端之間同步其 ecash 餘額。
什麼是 Cashu?
Cashu 是一種 ecash 協議:
- 隱私性:使用盲簽名技術,造幣廠無法追蹤交易
- 閃電網路整合:可與閃電網路相互轉換
- 離線支付:代幣可以離線傳輸
- 託管式:資金由造幣廠(Mint)託管
事件類型
| Kind | 說明 | 類型 |
|---|---|---|
17375 | 錢包事件 | 加密可替換 |
7375 | 代幣事件 | 加密一般 |
7376 | 交易歷史 | 加密一般 |
錢包事件 (Kind 17375)
錢包事件儲存錢包的配置和狀態,內容使用 NIP-44 加密。
標籤
| 標籤 | 說明 | 範例 |
|---|---|---|
d | 錢包識別符 | ["d", "my-wallet"] |
mint | 造幣廠 URL | ["mint", "https://mint.example.com"] |
relay | 代幣中繼器 | ["relay", "wss://relay.example.com"] |
加密內容結構
{
"mints": [
{
"url": "https://mint.example.com",
"balance": 1000,
"unit": "sat"
}
],
"proofs": []
} 代幣事件 (Kind 7375)
代幣事件儲存實際的 ecash 代幣,內容使用 NIP-44 加密。
標籤
| 標籤 | 說明 |
|---|---|
a | 關聯的錢包事件 |
加密內容(Cashu Token V4)
{
"mint": "https://mint.example.com",
"proofs": [
{
"amount": 1,
"id": "009a1f293253e41e",
"secret": "...",
"C": "..."
}
]
} 交易歷史 (Kind 7376)
記錄代幣的交易歷史,包括接收、發送、兌換等操作。
加密內容
{
"direction": "in",
"amount": 100,
"unit": "sat",
"mint": "https://mint.example.com",
"created_at": 1700000000,
"e": ["7375 事件 ID"]
} TypeScript 實作
錢包類別
import { nip44 } from 'nostr-tools';
import { CashuMint, CashuWallet, getEncodedToken } from '@cashu/cashu-ts';
interface WalletState {
mints: MintInfo[];
proofs: Proof[];
}
interface MintInfo {
url: string;
balance: number;
unit: string;
}
interface Proof {
amount: number;
id: string;
secret: string;
C: string;
}
class NostrCashuWallet {
private secretKey: Uint8Array;
private pubkey: string;
private walletId: string;
private relays: string[];
constructor(
secretKey: Uint8Array,
pubkey: string,
walletId: string = 'default',
relays: string[] = []
) {
this.secretKey = secretKey;
this.pubkey = pubkey;
this.walletId = walletId;
this.relays = relays;
}
// 加密錢包資料
private encrypt(data: any): string {
const plaintext = JSON.stringify(data);
return nip44.encrypt(plaintext, this.secretKey, this.pubkey);
}
// 解密錢包資料
private decrypt(ciphertext: string): any {
const plaintext = nip44.decrypt(ciphertext, this.secretKey, this.pubkey);
return JSON.parse(plaintext);
}
// 建立錢包事件
createWalletEvent(state: WalletState, mints: string[]) {
const content = this.encrypt(state);
const tags: string[][] = [['d', this.walletId]];
mints.forEach((mint) => {
tags.push(['mint', mint]);
});
this.relays.forEach((relay) => {
tags.push(['relay', relay]);
});
return {
kind: 17375,
content,
tags,
created_at: Math.floor(Date.now() / 1000),
};
}
// 建立代幣事件
createTokenEvent(mint: string, proofs: Proof[]) {
const token = {
mint,
proofs,
};
const content = this.encrypt(token);
return {
kind: 7375,
content,
tags: [
['a', `17375:${this.pubkey}:${this.walletId}`],
],
created_at: Math.floor(Date.now() / 1000),
};
}
// 建立歷史事件
createHistoryEvent(
direction: 'in' | 'out',
amount: number,
mint: string,
tokenEventId: string
) {
const history = {
direction,
amount,
unit: 'sat',
mint,
created_at: Math.floor(Date.now() / 1000),
e: [tokenEventId],
};
const content = this.encrypt(history);
return {
kind: 7376,
content,
tags: [
['a', `17375:${this.pubkey}:${this.walletId}`],
],
created_at: Math.floor(Date.now() / 1000),
};
}
} 從 Nostr 載入錢包
import { SimplePool } from 'nostr-tools';
async function loadWallet(
pool: SimplePool,
relays: string[],
secretKey: Uint8Array,
pubkey: string,
walletId: string = 'default'
): Promise<WalletState | null> {
// 獲取錢包事件
const walletEvents = await pool.querySync(relays, {
kinds: [17375],
authors: [pubkey],
'#d': [walletId],
});
if (walletEvents.length === 0) {
return null;
}
// 取最新的錢包事件
const walletEvent = walletEvents.sort(
(a, b) => b.created_at - a.created_at
)[0];
// 解密錢包狀態
const wallet = new NostrCashuWallet(secretKey, pubkey, walletId, relays);
const state = wallet.decrypt(walletEvent.content);
// 獲取代幣事件
const tokenEvents = await pool.querySync(relays, {
kinds: [7375],
authors: [pubkey],
'#a': [`17375:${pubkey}:${walletId}`],
});
// 解密並合併代幣
const allProofs: Proof[] = [];
for (const event of tokenEvents) {
try {
const token = wallet.decrypt(event.content);
allProofs.push(...token.proofs);
} catch (e) {
console.error('Failed to decrypt token:', e);
}
}
return {
...state,
proofs: allProofs,
};
} 接收 Cashu 代幣
import { getDecodedToken } from '@cashu/cashu-ts';
async function receiveToken(
wallet: NostrCashuWallet,
pool: SimplePool,
relays: string[],
cashuToken: string
) {
// 解碼 Cashu 代幣
const decoded = getDecodedToken(cashuToken);
// 驗證並兌換代幣
const mint = new CashuMint(decoded.mint);
const cashuWallet = new CashuWallet(mint);
const proofs = await cashuWallet.receive(decoded);
// 計算總額
const amount = proofs.reduce((sum, p) => sum + p.amount, 0);
// 建立代幣事件
const tokenEvent = wallet.createTokenEvent(decoded.mint, proofs);
// 發布到中繼器
await Promise.all(
relays.map((relay) =>
pool.publish([relay], finalizeEvent(tokenEvent, secretKey))
)
);
// 記錄歷史
const historyEvent = wallet.createHistoryEvent(
'in',
amount,
decoded.mint,
tokenEvent.id
);
await Promise.all(
relays.map((relay) =>
pool.publish([relay], finalizeEvent(historyEvent, secretKey))
)
);
return { amount, proofs };
} 發送 Cashu 代幣
async function sendToken(
wallet: NostrCashuWallet,
pool: SimplePool,
relays: string[],
mintUrl: string,
amount: number,
proofs: Proof[]
): Promise<string> {
const mint = new CashuMint(mintUrl);
const cashuWallet = new CashuWallet(mint);
// 選擇足夠的代幣
const selectedProofs = selectProofs(proofs, amount);
// 拆分代幣
const { send, returnChange } = await cashuWallet.send(
amount,
selectedProofs
);
// 編碼為 Cashu token
const token = getEncodedToken({
mint: mintUrl,
proofs: send,
});
// 更新錢包(移除已使用的代幣,添加找零)
// ... 發布更新的錢包事件
// 記錄歷史
const historyEvent = wallet.createHistoryEvent(
'out',
amount,
mintUrl,
'token-event-id'
);
await Promise.all(
relays.map((relay) =>
pool.publish([relay], finalizeEvent(historyEvent, secretKey))
)
);
return token;
}
function selectProofs(proofs: Proof[], amount: number): Proof[] {
// 簡單的代幣選擇演算法
const sorted = [...proofs].sort((a, b) => b.amount - a.amount);
const selected: Proof[] = [];
let total = 0;
for (const proof of sorted) {
if (total >= amount) break;
selected.push(proof);
total += proof.amount;
}
if (total < amount) {
throw new Error('Insufficient balance');
}
return selected;
} 安全考量
- 加密儲存:所有代幣使用 NIP-44 加密
- 造幣廠信任:Cashu 是託管式的,需要信任造幣廠
- 代幣備份:確保代幣事件正確發布到多個中繼器
- 雙花防護:已使用的代幣必須及時標記
使用場景
- 隱私支付:不可追蹤的小額支付
- 離線轉帳:無需網路即可轉移代幣
- 跨客戶端同步:在不同應用間共享餘額
- NutZaps:使用 Cashu 進行 Zaps(NIP-61)
相關 NIPs
參考資源
已複製連結