Add Claude Code skills for web frontend frameworks
- Add Svelte 3/4 skill covering components, reactivity, stores, lifecycle - Add Rollup skill covering configuration, plugins, code splitting - Add nostr-tools skill covering event creation, signing, relay communication - Add applesauce-core skill covering event stores, reactive queries - Add applesauce-signers skill covering NIP-07/NIP-46 signing abstractions - Update .gitignore to include .claude/** directory 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
767
.claude/skills/nostr-tools/SKILL.md
Normal file
767
.claude/skills/nostr-tools/SKILL.md
Normal file
@@ -0,0 +1,767 @@
|
||||
---
|
||||
name: nostr-tools
|
||||
description: This skill should be used when working with nostr-tools library for Nostr protocol operations, including event creation, signing, filtering, relay communication, and NIP implementations. Provides comprehensive knowledge of nostr-tools APIs and patterns.
|
||||
---
|
||||
|
||||
# nostr-tools Skill
|
||||
|
||||
This skill provides comprehensive knowledge and patterns for working with nostr-tools, the most popular JavaScript/TypeScript library for Nostr protocol development.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Building Nostr clients or applications
|
||||
- Creating and signing Nostr events
|
||||
- Connecting to Nostr relays
|
||||
- Implementing NIP features
|
||||
- Working with Nostr keys and cryptography
|
||||
- Filtering and querying events
|
||||
- Building relay pools or connections
|
||||
- Implementing NIP-44/NIP-04 encryption
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### nostr-tools Overview
|
||||
|
||||
nostr-tools provides:
|
||||
- **Event handling** - Create, sign, verify events
|
||||
- **Key management** - Generate, convert, encode keys
|
||||
- **Relay communication** - Connect, subscribe, publish
|
||||
- **NIP implementations** - NIP-04, NIP-05, NIP-19, NIP-44, etc.
|
||||
- **Cryptographic operations** - Schnorr signatures, encryption
|
||||
- **Filter building** - Query events by various criteria
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install nostr-tools
|
||||
```
|
||||
|
||||
### Basic Imports
|
||||
|
||||
```javascript
|
||||
// Core functionality
|
||||
import {
|
||||
SimplePool,
|
||||
generateSecretKey,
|
||||
getPublicKey,
|
||||
finalizeEvent,
|
||||
verifyEvent
|
||||
} from 'nostr-tools';
|
||||
|
||||
// NIP-specific imports
|
||||
import { nip04, nip05, nip19, nip44 } from 'nostr-tools';
|
||||
|
||||
// Relay operations
|
||||
import { Relay } from 'nostr-tools/relay';
|
||||
```
|
||||
|
||||
## Key Management
|
||||
|
||||
### Generating Keys
|
||||
|
||||
```javascript
|
||||
import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
|
||||
|
||||
// Generate new secret key (Uint8Array)
|
||||
const secretKey = generateSecretKey();
|
||||
|
||||
// Derive public key
|
||||
const publicKey = getPublicKey(secretKey);
|
||||
|
||||
console.log('Secret key:', bytesToHex(secretKey));
|
||||
console.log('Public key:', publicKey); // hex string
|
||||
```
|
||||
|
||||
### Key Encoding (NIP-19)
|
||||
|
||||
```javascript
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
// Encode to bech32
|
||||
const nsec = nip19.nsecEncode(secretKey);
|
||||
const npub = nip19.npubEncode(publicKey);
|
||||
const note = nip19.noteEncode(eventId);
|
||||
|
||||
console.log(nsec); // nsec1...
|
||||
console.log(npub); // npub1...
|
||||
console.log(note); // note1...
|
||||
|
||||
// Decode from bech32
|
||||
const { type, data } = nip19.decode(npub);
|
||||
// type: 'npub', data: publicKey (hex)
|
||||
|
||||
// Encode profile reference (nprofile)
|
||||
const nprofile = nip19.nprofileEncode({
|
||||
pubkey: publicKey,
|
||||
relays: ['wss://relay.example.com']
|
||||
});
|
||||
|
||||
// Encode event reference (nevent)
|
||||
const nevent = nip19.neventEncode({
|
||||
id: eventId,
|
||||
relays: ['wss://relay.example.com'],
|
||||
author: publicKey,
|
||||
kind: 1
|
||||
});
|
||||
|
||||
// Encode address (naddr) for replaceable events
|
||||
const naddr = nip19.naddrEncode({
|
||||
identifier: 'my-article',
|
||||
pubkey: publicKey,
|
||||
kind: 30023,
|
||||
relays: ['wss://relay.example.com']
|
||||
});
|
||||
```
|
||||
|
||||
## Event Operations
|
||||
|
||||
### Event Structure
|
||||
|
||||
```javascript
|
||||
// Unsigned event template
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'Hello Nostr!'
|
||||
};
|
||||
|
||||
// Signed event (after finalizeEvent)
|
||||
const signedEvent = {
|
||||
id: '...', // 32-byte sha256 hash as hex
|
||||
pubkey: '...', // 32-byte public key as hex
|
||||
created_at: 1234567890,
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Hello Nostr!',
|
||||
sig: '...' // 64-byte Schnorr signature as hex
|
||||
};
|
||||
```
|
||||
|
||||
### Creating and Signing Events
|
||||
|
||||
```javascript
|
||||
import { finalizeEvent, verifyEvent } from 'nostr-tools/pure';
|
||||
|
||||
// Create event template
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['p', publicKey], // Mention
|
||||
['e', eventId, '', 'reply'], // Reply
|
||||
['t', 'nostr'] // Hashtag
|
||||
],
|
||||
content: 'Hello Nostr!'
|
||||
};
|
||||
|
||||
// Sign event
|
||||
const signedEvent = finalizeEvent(eventTemplate, secretKey);
|
||||
|
||||
// Verify event
|
||||
const isValid = verifyEvent(signedEvent);
|
||||
console.log('Event valid:', isValid);
|
||||
```
|
||||
|
||||
### Event Kinds
|
||||
|
||||
```javascript
|
||||
// Common event kinds
|
||||
const KINDS = {
|
||||
Metadata: 0, // Profile metadata (NIP-01)
|
||||
Text: 1, // Short text note (NIP-01)
|
||||
RecommendRelay: 2, // Relay recommendation
|
||||
Contacts: 3, // Contact list (NIP-02)
|
||||
EncryptedDM: 4, // Encrypted DM (NIP-04)
|
||||
EventDeletion: 5, // Delete events (NIP-09)
|
||||
Repost: 6, // Repost (NIP-18)
|
||||
Reaction: 7, // Reaction (NIP-25)
|
||||
ChannelCreation: 40, // Channel (NIP-28)
|
||||
ChannelMessage: 42, // Channel message
|
||||
Zap: 9735, // Zap receipt (NIP-57)
|
||||
Report: 1984, // Report (NIP-56)
|
||||
RelayList: 10002, // Relay list (NIP-65)
|
||||
Article: 30023, // Long-form content (NIP-23)
|
||||
};
|
||||
```
|
||||
|
||||
### Creating Specific Events
|
||||
|
||||
```javascript
|
||||
// Profile metadata (kind 0)
|
||||
const profileEvent = finalizeEvent({
|
||||
kind: 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: JSON.stringify({
|
||||
name: 'Alice',
|
||||
about: 'Nostr enthusiast',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
nip05: 'alice@example.com',
|
||||
lud16: 'alice@getalby.com'
|
||||
})
|
||||
}, secretKey);
|
||||
|
||||
// Contact list (kind 3)
|
||||
const contactsEvent = finalizeEvent({
|
||||
kind: 3,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['p', pubkey1, 'wss://relay1.com', 'alice'],
|
||||
['p', pubkey2, 'wss://relay2.com', 'bob'],
|
||||
['p', pubkey3, '', 'carol']
|
||||
],
|
||||
content: '' // Or JSON relay preferences
|
||||
}, secretKey);
|
||||
|
||||
// Reply to an event
|
||||
const replyEvent = finalizeEvent({
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['e', rootEventId, '', 'root'],
|
||||
['e', parentEventId, '', 'reply'],
|
||||
['p', parentEventPubkey]
|
||||
],
|
||||
content: 'This is a reply'
|
||||
}, secretKey);
|
||||
|
||||
// Reaction (kind 7)
|
||||
const reactionEvent = finalizeEvent({
|
||||
kind: 7,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['e', eventId],
|
||||
['p', eventPubkey]
|
||||
],
|
||||
content: '+' // or '-' or emoji
|
||||
}, secretKey);
|
||||
|
||||
// Delete event (kind 5)
|
||||
const deleteEvent = finalizeEvent({
|
||||
kind: 5,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['e', eventIdToDelete],
|
||||
['e', anotherEventIdToDelete]
|
||||
],
|
||||
content: 'Deletion reason'
|
||||
}, secretKey);
|
||||
```
|
||||
|
||||
## Relay Communication
|
||||
|
||||
### Using SimplePool
|
||||
|
||||
SimplePool is the recommended way to interact with multiple relays:
|
||||
|
||||
```javascript
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
|
||||
const pool = new SimplePool();
|
||||
const relays = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.nostr.band'
|
||||
];
|
||||
|
||||
// Subscribe to events
|
||||
const subscription = pool.subscribeMany(
|
||||
relays,
|
||||
[
|
||||
{
|
||||
kinds: [1],
|
||||
authors: [publicKey],
|
||||
limit: 10
|
||||
}
|
||||
],
|
||||
{
|
||||
onevent(event) {
|
||||
console.log('Received event:', event);
|
||||
},
|
||||
oneose() {
|
||||
console.log('End of stored events');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Close subscription when done
|
||||
subscription.close();
|
||||
|
||||
// Publish event to all relays
|
||||
const results = await Promise.allSettled(
|
||||
pool.publish(relays, signedEvent)
|
||||
);
|
||||
|
||||
// Query events (returns Promise)
|
||||
const events = await pool.querySync(relays, {
|
||||
kinds: [0],
|
||||
authors: [publicKey]
|
||||
});
|
||||
|
||||
// Get single event
|
||||
const event = await pool.get(relays, {
|
||||
ids: [eventId]
|
||||
});
|
||||
|
||||
// Close pool when done
|
||||
pool.close(relays);
|
||||
```
|
||||
|
||||
### Direct Relay Connection
|
||||
|
||||
```javascript
|
||||
import { Relay } from 'nostr-tools/relay';
|
||||
|
||||
const relay = await Relay.connect('wss://relay.damus.io');
|
||||
|
||||
console.log(`Connected to ${relay.url}`);
|
||||
|
||||
// Subscribe
|
||||
const sub = relay.subscribe([
|
||||
{
|
||||
kinds: [1],
|
||||
limit: 100
|
||||
}
|
||||
], {
|
||||
onevent(event) {
|
||||
console.log('Event:', event);
|
||||
},
|
||||
oneose() {
|
||||
console.log('EOSE');
|
||||
sub.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Publish
|
||||
await relay.publish(signedEvent);
|
||||
|
||||
// Close
|
||||
relay.close();
|
||||
```
|
||||
|
||||
### Handling Connection States
|
||||
|
||||
```javascript
|
||||
import { Relay } from 'nostr-tools/relay';
|
||||
|
||||
const relay = await Relay.connect('wss://relay.example.com');
|
||||
|
||||
// Listen for disconnect
|
||||
relay.onclose = () => {
|
||||
console.log('Relay disconnected');
|
||||
};
|
||||
|
||||
// Check connection status
|
||||
console.log('Connected:', relay.connected);
|
||||
```
|
||||
|
||||
## Filters
|
||||
|
||||
### Filter Structure
|
||||
|
||||
```javascript
|
||||
const filter = {
|
||||
// Event IDs
|
||||
ids: ['abc123...'],
|
||||
|
||||
// Authors (pubkeys)
|
||||
authors: ['pubkey1', 'pubkey2'],
|
||||
|
||||
// Event kinds
|
||||
kinds: [1, 6, 7],
|
||||
|
||||
// Tags (single-letter keys)
|
||||
'#e': ['eventId1', 'eventId2'],
|
||||
'#p': ['pubkey1'],
|
||||
'#t': ['nostr', 'bitcoin'],
|
||||
'#d': ['article-identifier'],
|
||||
|
||||
// Time range
|
||||
since: 1704067200, // Unix timestamp
|
||||
until: 1704153600,
|
||||
|
||||
// Limit results
|
||||
limit: 100,
|
||||
|
||||
// Search (NIP-50, if relay supports)
|
||||
search: 'nostr protocol'
|
||||
};
|
||||
```
|
||||
|
||||
### Common Filter Patterns
|
||||
|
||||
```javascript
|
||||
// User's recent posts
|
||||
const userPosts = {
|
||||
kinds: [1],
|
||||
authors: [userPubkey],
|
||||
limit: 50
|
||||
};
|
||||
|
||||
// User's profile
|
||||
const userProfile = {
|
||||
kinds: [0],
|
||||
authors: [userPubkey]
|
||||
};
|
||||
|
||||
// User's contacts
|
||||
const userContacts = {
|
||||
kinds: [3],
|
||||
authors: [userPubkey]
|
||||
};
|
||||
|
||||
// Replies to an event
|
||||
const replies = {
|
||||
kinds: [1],
|
||||
'#e': [eventId]
|
||||
};
|
||||
|
||||
// Reactions to an event
|
||||
const reactions = {
|
||||
kinds: [7],
|
||||
'#e': [eventId]
|
||||
};
|
||||
|
||||
// Feed from followed users
|
||||
const feed = {
|
||||
kinds: [1, 6],
|
||||
authors: followedPubkeys,
|
||||
limit: 100
|
||||
};
|
||||
|
||||
// Events mentioning user
|
||||
const mentions = {
|
||||
kinds: [1],
|
||||
'#p': [userPubkey],
|
||||
limit: 50
|
||||
};
|
||||
|
||||
// Hashtag search
|
||||
const hashtagEvents = {
|
||||
kinds: [1],
|
||||
'#t': ['bitcoin'],
|
||||
limit: 100
|
||||
};
|
||||
|
||||
// Replaceable event by d-tag
|
||||
const replaceableEvent = {
|
||||
kinds: [30023],
|
||||
authors: [authorPubkey],
|
||||
'#d': ['article-slug']
|
||||
};
|
||||
```
|
||||
|
||||
### Multiple Filters
|
||||
|
||||
```javascript
|
||||
// Subscribe with multiple filters (OR logic)
|
||||
const filters = [
|
||||
{ kinds: [1], authors: [userPubkey], limit: 20 },
|
||||
{ kinds: [1], '#p': [userPubkey], limit: 20 }
|
||||
];
|
||||
|
||||
pool.subscribeMany(relays, filters, {
|
||||
onevent(event) {
|
||||
// Receives events matching ANY filter
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Encryption
|
||||
|
||||
### NIP-04 (Legacy DMs)
|
||||
|
||||
```javascript
|
||||
import { nip04 } from 'nostr-tools';
|
||||
|
||||
// Encrypt message
|
||||
const ciphertext = await nip04.encrypt(
|
||||
secretKey,
|
||||
recipientPubkey,
|
||||
'Hello, this is secret!'
|
||||
);
|
||||
|
||||
// Create encrypted DM event
|
||||
const dmEvent = finalizeEvent({
|
||||
kind: 4,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['p', recipientPubkey]],
|
||||
content: ciphertext
|
||||
}, secretKey);
|
||||
|
||||
// Decrypt message
|
||||
const plaintext = await nip04.decrypt(
|
||||
secretKey,
|
||||
senderPubkey,
|
||||
ciphertext
|
||||
);
|
||||
```
|
||||
|
||||
### NIP-44 (Modern Encryption)
|
||||
|
||||
```javascript
|
||||
import { nip44 } from 'nostr-tools';
|
||||
|
||||
// Get conversation key (cache this for multiple messages)
|
||||
const conversationKey = nip44.getConversationKey(
|
||||
secretKey,
|
||||
recipientPubkey
|
||||
);
|
||||
|
||||
// Encrypt
|
||||
const ciphertext = nip44.encrypt(
|
||||
'Hello with NIP-44!',
|
||||
conversationKey
|
||||
);
|
||||
|
||||
// Decrypt
|
||||
const plaintext = nip44.decrypt(
|
||||
ciphertext,
|
||||
conversationKey
|
||||
);
|
||||
```
|
||||
|
||||
## NIP Implementations
|
||||
|
||||
### NIP-05 (DNS Identifier)
|
||||
|
||||
```javascript
|
||||
import { nip05 } from 'nostr-tools';
|
||||
|
||||
// Query NIP-05 identifier
|
||||
const profile = await nip05.queryProfile('alice@example.com');
|
||||
|
||||
if (profile) {
|
||||
console.log('Pubkey:', profile.pubkey);
|
||||
console.log('Relays:', profile.relays);
|
||||
}
|
||||
|
||||
// Verify NIP-05 for a pubkey
|
||||
const isValid = await nip05.queryProfile('alice@example.com')
|
||||
.then(p => p?.pubkey === expectedPubkey);
|
||||
```
|
||||
|
||||
### NIP-10 (Reply Threading)
|
||||
|
||||
```javascript
|
||||
import { nip10 } from 'nostr-tools';
|
||||
|
||||
// Parse reply tags
|
||||
const parsed = nip10.parse(event);
|
||||
|
||||
console.log('Root:', parsed.root); // Original event
|
||||
console.log('Reply:', parsed.reply); // Direct parent
|
||||
console.log('Mentions:', parsed.mentions); // Other mentions
|
||||
console.log('Profiles:', parsed.profiles); // Mentioned pubkeys
|
||||
```
|
||||
|
||||
### NIP-21 (nostr: URIs)
|
||||
|
||||
```javascript
|
||||
// Parse nostr: URIs
|
||||
const uri = 'nostr:npub1...';
|
||||
const { type, data } = nip19.decode(uri.replace('nostr:', ''));
|
||||
```
|
||||
|
||||
### NIP-27 (Content References)
|
||||
|
||||
```javascript
|
||||
// Parse nostr:npub and nostr:note references in content
|
||||
const content = 'Check out nostr:npub1abc... and nostr:note1xyz...';
|
||||
|
||||
const references = content.match(/nostr:(n[a-z]+1[a-z0-9]+)/g);
|
||||
references?.forEach(ref => {
|
||||
const decoded = nip19.decode(ref.replace('nostr:', ''));
|
||||
console.log(decoded.type, decoded.data);
|
||||
});
|
||||
```
|
||||
|
||||
### NIP-57 (Zaps)
|
||||
|
||||
```javascript
|
||||
import { nip57 } from 'nostr-tools';
|
||||
|
||||
// Validate zap receipt
|
||||
const zapReceipt = await pool.get(relays, {
|
||||
kinds: [9735],
|
||||
'#e': [eventId]
|
||||
});
|
||||
|
||||
const validatedZap = await nip57.validateZapRequest(zapReceipt);
|
||||
```
|
||||
|
||||
## Utilities
|
||||
|
||||
### Hex and Bytes Conversion
|
||||
|
||||
```javascript
|
||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
||||
|
||||
// Convert secret key to hex
|
||||
const secretKeyHex = bytesToHex(secretKey);
|
||||
|
||||
// Convert hex back to bytes
|
||||
const secretKeyBytes = hexToBytes(secretKeyHex);
|
||||
```
|
||||
|
||||
### Event ID Calculation
|
||||
|
||||
```javascript
|
||||
import { getEventHash } from 'nostr-tools/pure';
|
||||
|
||||
// Calculate event ID without signing
|
||||
const eventId = getEventHash(unsignedEvent);
|
||||
```
|
||||
|
||||
### Signature Operations
|
||||
|
||||
```javascript
|
||||
import {
|
||||
getSignature,
|
||||
verifyEvent
|
||||
} from 'nostr-tools/pure';
|
||||
|
||||
// Sign event data
|
||||
const signature = getSignature(unsignedEvent, secretKey);
|
||||
|
||||
// Verify complete event
|
||||
const isValid = verifyEvent(signedEvent);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Connection Management
|
||||
|
||||
1. **Use SimplePool** - Manages connections efficiently
|
||||
2. **Limit concurrent connections** - Don't connect to too many relays
|
||||
3. **Handle disconnections** - Implement reconnection logic
|
||||
4. **Close subscriptions** - Always close when done
|
||||
|
||||
### Event Handling
|
||||
|
||||
1. **Verify events** - Always verify signatures
|
||||
2. **Deduplicate** - Events may come from multiple relays
|
||||
3. **Handle replaceable events** - Latest by created_at wins
|
||||
4. **Validate content** - Don't trust event content blindly
|
||||
|
||||
### Key Security
|
||||
|
||||
1. **Never expose secret keys** - Keep in secure storage
|
||||
2. **Use NIP-07 in browsers** - Let extensions handle signing
|
||||
3. **Validate input** - Check key formats before use
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Cache events** - Avoid re-fetching
|
||||
2. **Use filters wisely** - Be specific, use limits
|
||||
3. **Batch operations** - Combine related queries
|
||||
4. **Close idle connections** - Free up resources
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Building a Feed
|
||||
|
||||
```javascript
|
||||
const pool = new SimplePool();
|
||||
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
|
||||
|
||||
async function loadFeed(followedPubkeys) {
|
||||
const events = await pool.querySync(relays, {
|
||||
kinds: [1, 6],
|
||||
authors: followedPubkeys,
|
||||
limit: 100
|
||||
});
|
||||
|
||||
// Sort by timestamp
|
||||
return events.sort((a, b) => b.created_at - a.created_at);
|
||||
}
|
||||
```
|
||||
|
||||
### Real-time Updates
|
||||
|
||||
```javascript
|
||||
function subscribeToFeed(followedPubkeys, onEvent) {
|
||||
return pool.subscribeMany(
|
||||
relays,
|
||||
[{ kinds: [1, 6], authors: followedPubkeys }],
|
||||
{
|
||||
onevent: onEvent,
|
||||
oneose() {
|
||||
console.log('Caught up with stored events');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Profile Loading
|
||||
|
||||
```javascript
|
||||
async function loadProfile(pubkey) {
|
||||
const [metadata] = await pool.querySync(relays, {
|
||||
kinds: [0],
|
||||
authors: [pubkey],
|
||||
limit: 1
|
||||
});
|
||||
|
||||
if (metadata) {
|
||||
return JSON.parse(metadata.content);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Event Deduplication
|
||||
|
||||
```javascript
|
||||
const seenEvents = new Set();
|
||||
|
||||
function handleEvent(event) {
|
||||
if (seenEvents.has(event.id)) {
|
||||
return; // Skip duplicate
|
||||
}
|
||||
seenEvents.add(event.id);
|
||||
|
||||
// Process event...
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Events not publishing:**
|
||||
- Check relay is writable
|
||||
- Verify event is properly signed
|
||||
- Check relay's accepted kinds
|
||||
|
||||
**Subscription not receiving events:**
|
||||
- Verify filter syntax
|
||||
- Check relay has matching events
|
||||
- Ensure subscription isn't closed
|
||||
|
||||
**Signature verification fails:**
|
||||
- Check event structure is correct
|
||||
- Verify keys are in correct format
|
||||
- Ensure event hasn't been modified
|
||||
|
||||
**NIP-05 lookup fails:**
|
||||
- Check CORS headers on server
|
||||
- Verify .well-known path is correct
|
||||
- Handle network timeouts
|
||||
|
||||
## References
|
||||
|
||||
- **nostr-tools GitHub**: https://github.com/nbd-wtf/nostr-tools
|
||||
- **Nostr Protocol**: https://github.com/nostr-protocol/nostr
|
||||
- **NIPs Repository**: https://github.com/nostr-protocol/nips
|
||||
- **NIP-01 (Basic Protocol)**: https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **nostr** - Nostr protocol fundamentals
|
||||
- **svelte** - Building Nostr UIs with Svelte
|
||||
- **applesauce-core** - Higher-level Nostr client utilities
|
||||
- **applesauce-signers** - Nostr signing abstractions
|
||||
Reference in New Issue
Block a user