跳至主要內容
進階

NIP-58 徽章

Nostr 徽章系統,用於頒發和展示成就、認證與身份標識。

8 分鐘

概述

NIP-58 定義了 Nostr 的徽章系統,讓用戶和組織可以創建、頒發和展示徽章。 徽章可用於表彰成就、認證身份或標識社群成員。這套機制由三個事件類型組成: 徽章定義、徽章頒發和個人徽章展示。

事件類型

Kind 名稱 用途
30009 Badge Definition 定義徽章的名稱、描述、圖片
8 Badge Award 頒發徽章給用戶
30008 Profile Badges 用戶展示已接受的徽章

徽章定義 (Kind 30009)

徽章定義是一個可替換事件,使用 d 標籤作為唯一識別符。 任何人都可以創建徽章,但徽章的價值取決於頒發者的聲譽。

事件結構

{`{
  "kind": 30009,
  "tags": [
    ["d", "bitcoin-developer"],
    ["name", "Bitcoin 開發者"],
    ["description", "對 Bitcoin Core 有貢獻的開發者"],
    ["image", "https://example.com/badges/btc-dev.png", "1024x1024"],
    ["thumb", "https://example.com/badges/btc-dev-thumb.png", "256x256"]
  ],
  "content": "",
  "pubkey": "<頒發者公鑰>",
  ...
}`}

標籤說明

  • d - 徽章識別符(必需),用於組成 naddr
  • name - 徽章名稱(必需)
  • description - 徽章描述
  • image - 徽章圖片 URL,可選擇加上尺寸
  • thumb - 縮圖 URL,用於列表顯示

TypeScript 範例

{`import { finalizeEvent, getPublicKey } from 'nostr-tools';

// 創建徽章定義
function createBadgeDefinition(
  privateKey: Uint8Array,
  badge: {
    id: string;
    name: string;
    description: string;
    image: string;
    thumb?: string;
  }
) {
  const tags: string[][] = [
    ['d', badge.id],
    ['name', badge.name],
    ['description', badge.description],
    ['image', badge.image],
  ];

  if (badge.thumb) {
    tags.push(['thumb', badge.thumb]);
  }

  const event = {
    kind: 30009,
    created_at: Math.floor(Date.now() / 1000),
    tags,
    content: '',
  };

  return finalizeEvent(event, privateKey);
}

// 使用範例
const badgeDef = createBadgeDefinition(privateKey, {
  id: 'early-adopter',
  name: '早期採用者',
  description: '2024 年之前加入的用戶',
  image: 'https://example.com/early-adopter.png',
  thumb: 'https://example.com/early-adopter-thumb.png',
});`}

徽章頒發 (Kind 8)

徽章頒發事件將特定徽章授予一個或多個用戶。只有徽章定義的創建者才能頒發該徽章。

事件結構

{`{
  "kind": 8,
  "tags": [
    ["a", "30009:<頒發者公鑰>:bitcoin-developer"],
    ["p", "<獲獎用戶1公鑰>", "wss://relay.example.com"],
    ["p", "<獲獎用戶2公鑰>", "wss://relay.example.com"]
  ],
  "content": "",
  "pubkey": "<頒發者公鑰>",
  ...
}`}

標籤說明

  • a - 指向徽章定義的地址標籤(必需)
  • p - 獲獎用戶的公鑰,可包含多個

TypeScript 範例

{`// 頒發徽章
function awardBadge(
  privateKey: Uint8Array,
  badgeId: string,
  recipients: { pubkey: string; relay?: string }[]
) {
  const issuerPubkey = getPublicKey(privateKey);

  const tags: string[][] = [
    ['a', \`30009:\${issuerPubkey}:\${badgeId}\`],
  ];

  for (const recipient of recipients) {
    if (recipient.relay) {
      tags.push(['p', recipient.pubkey, recipient.relay]);
    } else {
      tags.push(['p', recipient.pubkey]);
    }
  }

  const event = {
    kind: 8,
    created_at: Math.floor(Date.now() / 1000),
    tags,
    content: '',
  };

  return finalizeEvent(event, privateKey);
}

// 頒發徽章給多個用戶
const awardEvent = awardBadge(privateKey, 'early-adopter', [
  { pubkey: 'abc123...', relay: 'wss://relay.damus.io' },
  { pubkey: 'def456...', relay: 'wss://nos.lol' },
]);`}

個人徽章展示 (Kind 30008)

用戶可以選擇在個人檔案中展示哪些已獲得的徽章。這是一個可替換事件, 使用 d 標籤值為 profile_badges

事件結構

{`{
  "kind": 30008,
  "tags": [
    ["d", "profile_badges"],
    ["a", "30009:<頒發者1>:bitcoin-developer"],
    ["e", "<對應的頒發事件 ID>", "wss://relay.example.com"],
    ["a", "30009:<頒發者2>:early-adopter"],
    ["e", "<對應的頒發事件 ID>", "wss://relay.example.com"]
  ],
  "content": "",
  ...
}`}

重要規則

  • ae 標籤必須成對出現
  • a 指向徽章定義,e 指向頒發事件
  • 順序決定顯示優先級(越前面越重要)
  • 客戶端應驗證頒發事件確實包含該用戶

TypeScript 範例

{`// 設定個人展示的徽章
function setProfileBadges(
  privateKey: Uint8Array,
  badges: { badgeAddr: string; awardEventId: string; relay: string }[]
) {
  const tags: string[][] = [['d', 'profile_badges']];

  for (const badge of badges) {
    tags.push(['a', badge.badgeAddr]);
    tags.push(['e', badge.awardEventId, badge.relay]);
  }

  const event = {
    kind: 30008,
    created_at: Math.floor(Date.now() / 1000),
    tags,
    content: '',
  };

  return finalizeEvent(event, privateKey);
}

// 使用範例
const profileBadges = setProfileBadges(privateKey, [
  {
    badgeAddr: '30009:abc123...:bitcoin-developer',
    awardEventId: 'event123...',
    relay: 'wss://relay.damus.io',
  },
  {
    badgeAddr: '30009:def456...:early-adopter',
    awardEventId: 'event456...',
    relay: 'wss://nos.lol',
  },
]);`}

查詢徽章

{`import { SimplePool } from 'nostr-tools';

const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];

// 查詢某人創建的所有徽章定義
async function getBadgeDefinitions(issuerPubkey: string) {
  return await pool.querySync(relays, {
    kinds: [30009],
    authors: [issuerPubkey],
  });
}

// 查詢某人獲得的所有徽章頒發
async function getReceivedAwards(userPubkey: string) {
  return await pool.querySync(relays, {
    kinds: [8],
    '#p': [userPubkey],
  });
}

// 查詢用戶的個人徽章展示
async function getProfileBadges(userPubkey: string) {
  const events = await pool.querySync(relays, {
    kinds: [30008],
    authors: [userPubkey],
    '#d': ['profile_badges'],
    limit: 1,
  });

  return events[0] || null;
}

// 驗證徽章頒發是否有效
async function validateBadgeAward(
  userPubkey: string,
  badgeAddr: string,
  awardEventId: string
) {
  // 1. 取得頒發事件
  const awardEvents = await pool.querySync(relays, {
    ids: [awardEventId],
    kinds: [8],
  });

  if (awardEvents.length === 0) return false;

  const award = awardEvents[0];

  // 2. 檢查頒發事件是否包含該用戶
  const hasUser = award.tags.some(
    t => t[0] === 'p' && t[1] === userPubkey
  );
  if (!hasUser) return false;

  // 3. 檢查頒發事件是否指向正確的徽章
  const hasBadge = award.tags.some(
    t => t[0] === 'a' && t[1] === badgeAddr
  );
  if (!hasBadge) return false;

  // 4. 檢查頒發者是否是徽章創建者
  const [, issuerFromAddr] = badgeAddr.split(':');
  return award.pubkey === issuerFromAddr;
}`}

完整顯示流程

客戶端展示用戶徽章的建議流程:

  1. 查詢用戶的 kind:30008(profile_badges)事件
  2. 解析所有 ae 標籤對
  3. 對每個徽章,驗證頒發事件的有效性
  4. 查詢徽章定義取得名稱和圖片
  5. 按順序顯示已驗證的徽章
{`// 完整的徽章載入流程
async function loadUserBadges(userPubkey: string) {
  // 1. 取得用戶的個人徽章設定
  const profileBadges = await getProfileBadges(userPubkey);
  if (!profileBadges) return [];

  const badges = [];
  const tags = profileBadges.tags;

  // 2. 解析 a/e 標籤對
  for (let i = 0; i < tags.length - 1; i++) {
    if (tags[i][0] === 'a' && tags[i + 1][0] === 'e') {
      const badgeAddr = tags[i][1];
      const awardEventId = tags[i + 1][1];

      // 3. 驗證頒發
      const isValid = await validateBadgeAward(
        userPubkey,
        badgeAddr,
        awardEventId
      );

      if (isValid) {
        // 4. 取得徽章定義
        const [kind, issuer, identifier] = badgeAddr.split(':');
        const defs = await pool.querySync(relays, {
          kinds: [30009],
          authors: [issuer],
          '#d': [identifier],
          limit: 1,
        });

        if (defs.length > 0) {
          const def = defs[0];
          badges.push({
            id: identifier,
            name: def.tags.find(t => t[0] === 'name')?.[1],
            image: def.tags.find(t => t[0] === 'image')?.[1],
            thumb: def.tags.find(t => t[0] === 'thumb')?.[1],
            issuer,
          });
        }
      }
    }
  }

  return badges;
}`}

應用場景

  • 社群認證:驗證用戶是特定組織的成員
  • 成就系統:標記用戶達成的里程碑
  • 技能認證:證明用戶具備特定技能或資格
  • 活動紀念:記錄用戶參與過的活動
  • 早期採用者:標識平台的早期支持者
  • NIP-01 - 基本協議與事件結構
  • NIP-19 - bech32 編碼(naddr 用於徽章地址)
  • NIP-51 - 列表(類似的可替換事件模式)

參考資源

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