SkateSpots/skills/npub-cash-address/SKILL.md
shakespeare.diy 27e14994b5 Import files from skate-spots project
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 20:04:15 +07:00

958 lines
31 KiB
Markdown

---
name: npub-cash-address
description: Use when implementing npub.cash static Lightning address functionality - provides complete patterns for obtaining static Lightning addresses (username@npubx.cash), managing npub.cash account settings, syncing quotes from npub.cash, and integrating with Lightning wallets
when_to_use: When building Lightning wallet features that need static addresses, implementing npub.cash integration, managing user Lightning addresses, or syncing quotes from npub.cash to Cashu wallets
---
# npub.cash Static Lightning Address Implementation
## Overview
Complete implementation guide for integrating with npub.cash to obtain static Lightning addresses for users. npub.cash provides a service that generates static Lightning addresses in the format `username@npubx.cash` based on a user's Nostr public key, allowing users to receive Lightning payments at a permanent address.
**Core Capabilities:**
- Obtain static Lightning address from npub.cash (`username@npubx.cash`)
- Manage npub.cash account settings (mint URL, lock quotes)
- Sync quotes from npub.cash to Cashu wallet
- Real-time quote subscription and updates
- NIP-98 authentication with npub.cash API
- Automatic mint management when syncing quotes
**Note:** This skill is **optional** for `lightning-wallet` implementations. Users can optionally use npub.cash to get a static Lightning address, but it's not required for basic Lightning wallet functionality.
## Prerequisites
**IMPORTANT:** Before adding dependencies, review your project's `package.json` to check if any of these packages already exist. If they do, verify the versions are compatible with the requirements below. Only add packages that are missing or need version updates.
**Required packages:**
- `npubcash-sdk@^0.2.0` - Official npub.cash SDK (minimum version 0.2.0, later versions acceptable)
- `npubcash-types` - TypeScript types for npub.cash (if not included in SDK)
- `nostr-tools@^2.13.0` - Nostr protocol utilities (for NIP-98 authentication)
**Required skills (must be referenced/implemented):**
- `qr-code-generator` - QR code generation for displaying Lightning addresses (see `qr-code-generator` skill)
- `lightning-wallet` - Lightning wallet operations (optional integration target)
**Optional dependencies:**
- NIP-98 authentication hook (for Nostr-based authentication)
## Implementation Checklist
- [ ] Set up NIP-98 authentication with npub.cash
- [ ] Initialize npubcash-sdk client
- [ ] Implement user info retrieval (get static Lightning address)
- [ ] Implement QR code generation for Lightning address (see `qr-code-generator` skill)
- [ ] Implement copy to clipboard functionality (see `bitcoin-wallet` skill for copy hook)
- [ ] Implement mint URL management
- [ ] Implement quote syncing from npub.cash
- [ ] Add real-time quote subscription
- [ ] Integrate with Lightning wallet (optional)
- [ ] Add error handling and user feedback
## Part 1: Understanding npub.cash Static Addresses
### Static Address vs Dynamic Invoices
**Static Lightning Address:**
- Format: `username@npubx.cash`
- Permanent address tied to user's Nostr pubkey
- Others can send payments to this address at any time
- npub.cash generates invoices on-demand when payments are received
- User doesn't need to create invoices manually
**Dynamic Invoices:**
- Created on-demand by user
- Expire after a set time
- User must generate and share each invoice
- More control but less convenient for recurring payments
### npub.cash Service
npub.cash provides:
- **Static addresses**: `username@npubx.cash` format
- **Quote management**: Tracks pending Lightning payments
- **Mint integration**: Links user's preferred Cashu mint
- **Real-time updates**: WebSocket subscription for quote updates
### Difference from lightning-address Skill
**This skill (`npub-cash-address`):**
- **Obtains** a static Lightning address from npub.cash
- Creates/manages the user's address on npub.cash
- Syncs quotes from npub.cash to wallet
**`lightning-address` skill:**
- **Resolves** any Lightning address to an invoice
- Takes an address (like `alice@strike.me`) and gets an invoice
- Works with any Lightning address provider
## Part 2: Core Implementation
### NIP-98 Authentication Setup
**Set up NIP-98 authentication for npub.cash API:**
```typescript
// hooks/useNIP98.ts (prerequisite)
import { useCallback } from 'react';
import { useCurrentUser } from './useCurrentUser';
import type { NostrEvent } from '@nostrify/nostrify';
export interface NIP98Options {
url: string;
method: string;
body?: unknown;
}
/**
* Hook for creating NIP-98 HTTP Authentication headers
* Based on https://github.com/nostr-protocol/nips/blob/master/98.md
*/
export function useNIP98() {
const { user } = useCurrentUser();
/**
* Create a NIP-98 authorization header for HTTP requests
* @param options - The request details (url, method, optional body)
* @returns Promise<string> - The "Nostr base64..." authorization header value
*/
const createAuthHeader = useCallback(async (options: NIP98Options): Promise<string> => {
if (!user) {
throw new Error('User must be logged in to create NIP-98 auth header');
}
const { url, method, body } = options;
// Parse URL to remove query parameters (per NIP-98 spec)
const urlObj = new URL(url);
const urlWithoutQuery = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
const tags: string[][] = [
['u', urlWithoutQuery],
['method', method.toUpperCase()],
];
// If there's a request body (POST, PUT, PATCH), add payload hash
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
const bodyString = JSON.stringify(body);
const encoder = new TextEncoder();
const data = encoder.encode(bodyString);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const payloadHash = Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
tags.push(['payload', payloadHash]);
}
// Create and sign the NIP-98 event (kind 27235)
const event = await user.signer.signEvent({
kind: 27235,
content: '',
tags,
created_at: Math.floor(Date.now() / 1000),
}) as NostrEvent;
// Encode the event as base64
const eventJson = JSON.stringify(event);
const base64Event = btoa(eventJson);
return `Nostr ${base64Event}`;
}, [user]);
return {
createAuthHeader,
isLoggedIn: !!user,
pubkey: user?.pubkey,
signer: user?.signer,
};
}
```
**Key Points:**
- **NIP-98 Event**: Creates a kind 27235 event with URL and method tags
- **URL Parsing**: Removes query parameters per NIP-98 specification
- **Payload Hashing**: For POST/PUT/PATCH requests, includes SHA-256 hash of request body
- **Base64 Encoding**: Encodes signed event as base64 for HTTP Authorization header
- **Format**: Returns `"Nostr {base64Event}"` format for Authorization header
### npub.cash Hook Implementation
**Complete hook for npub.cash integration:**
```typescript
// hooks/wallet/useNpubCash.ts
import { useCallback, useState, useMemo, useRef } from 'react';
import { useNIP98 } from '../useNIP98';
import { NPCClient, JWTAuthProvider } from 'npubcash-sdk';
import type { Quote } from 'npubcash-types';
import type { Manager } from 'coco-cashu-core';
import type { EventTemplate, Event } from 'nostr-tools';
// optional import { useToast } from '@/hooks/useToast';
const NPUBCASH_BASE_URL = 'https://npubx.cash';
export interface NpubCashUserInfo {
pubkey: string;
name?: string;
mintUrl: string; // API returns camelCase
lockQuote: boolean; // API returns camelCase
}
/**
* Hook for interacting with npub.cash API using the official SDK
*/
export function useNpubCash() {
const { createAuthHeader, signer, isLoggedIn, pubkey } = useNIP98();
// Optional: User feedback notifications
// Option 1: Console logging
// const logMessage = (message: string) => console.log(message);
// Option 2: Toast notifications (if useToast hook is available)
// const { toast } = useToast();
// Option 3: No notification handler
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Ref to track last sync timestamp for incremental updates
const lastSyncRef = useRef<number>(0);
// Create NPCClient instance using the SDK with JWTAuthProvider for proper WebSocket auth
const npcClient = useMemo(() => {
if (!isLoggedIn || !signer) return null;
// Adapt NostrSigner to JWTAuthProvider's expected SigningFunc signature
const signingFunc = async (eventTemplate: EventTemplate): Promise<Event> => {
return await signer.signEvent(eventTemplate);
};
const auth = new JWTAuthProvider(NPUBCASH_BASE_URL, signingFunc);
return new NPCClient(NPUBCASH_BASE_URL, auth);
}, [isLoggedIn, signer]);
// Generic wrapper for SDK calls with loading state
const withLoading = useCallback(async <T>(operation: () => Promise<T>): Promise<T | null> => {
try {
setIsLoading(true);
setError(null);
return await operation();
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
return null;
} finally {
setIsLoading(false);
}
}, []);
const setMint = useCallback(async (mintUrl: string): Promise<boolean> => {
if (!npcClient) return false;
const result = await withLoading(() => npcClient.settings.setMintUrl(mintUrl));
return result !== null;
}, [npcClient, withLoading]);
const getUserInfo = useCallback(async (): Promise<NpubCashUserInfo | null> => {
if (!npcClient) return null;
// Since SDK doesn't have getUserInfo, we'll use manual implementation
return withLoading(async () => {
const url = `${NPUBCASH_BASE_URL}/api/v2/user/info`;
const authHeader = await createAuthHeader({ url, method: 'GET' });
const response = await fetch(url, {
method: 'GET',
headers: { 'Authorization': authHeader, 'Content-Type': 'application/json' },
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Failed to get user info (${response.status})`);
}
const data = await response.json();
return data.data.user;
});
}, [npcClient, createAuthHeader, withLoading]);
const getAllQuotes = useCallback(async (): Promise<Quote[]> => {
if (!npcClient) return [];
const result = await withLoading(() => npcClient.getAllQuotes());
return result || [];
}, [npcClient, withLoading]);
const getQuotesSince = useCallback(async (since: number): Promise<Quote[]> => {
if (!npcClient) return [];
const result = await withLoading(() => npcClient.getQuotesSince(since));
return result || [];
}, [npcClient, withLoading]);
const setLockQuotes = useCallback(async (lockQuotes: boolean): Promise<boolean> => {
if (!npcClient) return false;
const result = await withLoading(() => npcClient.settings.setLock(lockQuotes));
return result !== null;
}, [npcClient, withLoading]);
const subscribeToQuotes = useCallback((
onUpdate: (quoteId: string) => void,
onError?: (msg: string) => void
) => {
if (!npcClient) return () => {};
return npcClient.subscribe(onUpdate, onError);
}, [npcClient]);
const clearError = useCallback(() => setError(null), []);
return {
// Core methods
setMint,
getUserInfo,
// Quote management methods
getAllQuotes,
getQuotesSince,
setLockQuotes,
subscribeToQuotes,
// Status
isLoading,
error,
isLoggedIn,
pubkey,
clearError,
// SDK instance for advanced usage
npcClient,
};
}
```
## Part 3: Getting Static Lightning Address
### Retrieving User Info
**Get user's static Lightning address from npub.cash:**
```typescript
// In your component
const { getUserInfo, isLoading, error } = useNpubCash();
// Get user info (includes static address)
const userInfo = await getUserInfo();
if (userInfo) {
// Extract username from pubkey or use name if available
const username = userInfo.name || extractUsernameFromPubkey(userInfo.pubkey);
const staticAddress = `${username}@npubx.cash`;
console.log('Static Lightning address:', staticAddress);
// Example: "alice@npubx.cash"
}
```
### Displaying Static Address
**Show static address in wallet UI with QR code and copy functionality:**
```typescript
// components/StaticAddressDisplay.tsx
import { useEffect, useState } from 'react';
import { useNpubCash } from '@/hooks/wallet/useNpubCash';
import { useQRCodeGenerator } from '@/hooks/useQRCodeGenerator';
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard';
import { Button } from '@/components/ui/button';
import { Copy, Check } from 'lucide-react';
import { QRModal } from '@/components/QRModal';
export function StaticAddressDisplay() {
const { getUserInfo, isLoading } = useNpubCash();
const { generateQRCode, isGenerating: isGeneratingQR } = useQRCodeGenerator();
const { copy, copied } = useCopyToClipboard();
const [staticAddress, setStaticAddress] = useState<string | null>(null);
const [qrCodeUrl, setQrCodeUrl] = useState<string>('');
const [showQRModal, setShowQRModal] = useState(false);
useEffect(() => {
const fetchAddress = async () => {
const userInfo = await getUserInfo();
if (userInfo) {
const username = userInfo.name || extractUsernameFromPubkey(userInfo.pubkey);
const address = `${username}@npubx.cash`;
setStaticAddress(address);
// Generate QR code for the address
const qrUrl = await generateQRCode(address);
setQrCodeUrl(qrUrl);
}
};
fetchAddress();
}, [getUserInfo, generateQRCode]);
if (isLoading) {
return <div>Loading address...</div>;
}
if (!staticAddress) {
return <div>No address available</div>;
}
return (
<div className="space-y-4">
<h3>Your Lightning Address</h3>
{/* Address display with copy button */}
<div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
<code className="flex-1 text-sm break-all">{staticAddress}</code>
<Button
variant="ghost"
size="icon"
onClick={() => copy(staticAddress)}
className="h-8 w-8 shrink-0"
aria-label="Copy address"
>
{copied ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
{/* QR code display */}
{qrCodeUrl && (
<div className="flex flex-col items-center gap-2">
<img
src={qrCodeUrl}
alt="QR Code"
className="w-48 h-48 cursor-pointer"
onClick={() => setShowQRModal(true)}
/>
<Button
variant="outline"
onClick={() => setShowQRModal(true)}
disabled={isGeneratingQR}
>
{isGeneratingQR ? 'Generating...' : 'View QR Code'}
</Button>
</div>
)}
{/* QR Modal */}
<QRModal
isOpen={showQRModal}
onClose={() => setShowQRModal(false)}
qrCodeUrl={qrCodeUrl}
content={staticAddress}
title="Lightning Address QR Code"
description="Scan this QR code to send Lightning payments"
/>
</div>
);
}
```
**Key Features:**
- **QR Code Generation** - Uses `qr-code-generator` skill to create QR codes for the Lightning address
- **Copy to Clipboard** - Uses copy hook from `bitcoin-wallet` skill for easy address copying
- **Visual Feedback** - Shows checkmark when address is copied
- **QR Modal** - Full-screen QR code view with copy functionality
- **Accessibility** - Proper ARIA labels and keyboard navigation
### Username Extraction
**Extract username from Nostr pubkey:**
```typescript
// lib/npubCashUtils.ts
export function extractUsernameFromPubkey(pubkey: string): string {
// npub.cash uses a deterministic username based on pubkey
// This is typically the first 8-12 characters of the npub
// You may need to check npub.cash API documentation for exact format
// Example: Use first part of pubkey as username
// In practice, npub.cash may provide this in the user info
return pubkey.substring(0, 8);
}
// Or use the name from userInfo if available
const username = userInfo.name || extractUsernameFromPubkey(userInfo.pubkey);
```
## Part 4: Managing npub.cash Settings
### Setting Mint URL
**Configure user's preferred mint on npub.cash:**
```typescript
// In your component
const { setMint, isLoading } = useNpubCash();
// Set mint URL (links npub.cash to user's preferred Cashu mint)
const success = await setMint('https://mint.example.com');
if (success) {
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.log('Mint Updated: Your npub.cash mint has been updated');
// Option 2: Toast notification (if toast is available)
// toast({ title: 'Mint Updated', description: 'Your npub.cash mint has been updated' });
// Option 3: No notification (silent success)
}
```
### Locking Quotes
**Prevent quote modifications (optional security feature):**
```typescript
// In your component
const { setLockQuotes, isLoading } = useNpubCash();
// Lock quotes to prevent modifications
const success = await setLockQuotes(true);
if (success) {
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.log('Quotes Locked: Your quotes are now locked');
// Option 2: Toast notification (if toast is available)
// toast({ title: 'Quotes Locked', description: 'Your quotes are now locked' });
// Option 3: No notification (silent success)
}
```
## Part 5: Quote Syncing with Lightning Wallet
### Syncing Quotes from npub.cash
**Sync quotes from npub.cash to Cashu wallet:**
```typescript
// hooks/wallet/useNpubCash.ts (excerpt)
const refreshQuotes = useCallback(async (coco: Manager): Promise<void> => {
if (!npcClient || !coco) return;
try {
// Fetch all quotes from npub.cash
const quotes = await getAllQuotes();
if (quotes.length === 0) {
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.log('No Quotes Found: No pending quotes found on npub.cash');
// Option 2: Toast notification (if toast is available)
// toast({ title: "No Quotes Found", description: "No pending quotes found on npub.cash" });
// Option 3: No notification (silent)
return;
}
// Group quotes by mint URL
const quotesByMint = quotes.reduce((acc, quote) => {
if (!acc[quote.mintUrl]) {
acc[quote.mintUrl] = [];
}
acc[quote.mintUrl].push(quote);
return acc;
}, {} as Record<string, typeof quotes>);
let addedQuotes = 0;
// Process each mint's quotes
for (const [mintUrl, mintQuotes] of Object.entries(quotesByMint)) {
try {
// Check if mint exists (known) by checking all mints
const allMints = await coco.mint.getAllMints();
const isKnown = allMints.some(m => m.mintUrl.toLowerCase() === mintUrl.toLowerCase());
if (!isKnown) {
await coco.mint.addMint(mintUrl, { trusted: true });
} else {
// Mint exists, ensure it's trusted
const isTrusted = await coco.mint.isTrustedMint(mintUrl);
if (!isTrusted) {
await coco.mint.trustMint(mintUrl);
}
}
// Add quotes to the wallet
coco.quotes.addMintQuote(
mintUrl,
mintQuotes.map((q) => ({
...q,
expiry: q.expiresAt,
quote: q.quoteId,
state: "PAID",
unit: "sat",
})),
);
addedQuotes += mintQuotes.length;
} catch (err) {
console.warn(`Failed to process quotes for mint ${mintUrl}:`, err);
}
}
if (addedQuotes > 0) {
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.log(`Quotes Refreshed: Successfully added ${addedQuotes} quotes from npub.cash`);
// Option 2: Toast notification (if toast is available)
// toast({ title: "Quotes Refreshed", description: `Successfully added ${addedQuotes} quotes from npub.cash` });
// Option 3: No notification (silent success)
}
} catch (err) {
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.error('Quote Refresh Failed:', err instanceof Error ? err.message : 'Unknown error');
// Option 2: Toast notification (if toast is available)
// toast({ variant: "destructive", title: "Refresh Failed", description: err instanceof Error ? err.message : 'Failed to refresh quotes' });
// Option 3: No notification (silent failure)
}
}, [npcClient, getAllQuotes]);
```
### Real-Time Quote Subscription
**Subscribe to real-time quote updates:**
```typescript
// hooks/wallet/useNpubCash.ts (excerpt)
const syncQuotesWithWallet = useCallback(async (coco: Manager): Promise<() => void> => {
if (!npcClient || !coco) return () => {};
try {
// Fetch and sync all quotes initially
const quotes = await getAllQuotes();
if (quotes.length > 0) {
// ... sync initial quotes (see refreshQuotes implementation)
}
// Subscribe to real-time updates
const unsubscribe = subscribeToQuotes(
async (quoteId: string) => {
try {
// When a quote is updated, fetch newer quotes and sync
const recentQuotes = await getQuotesSince(lastSyncRef.current);
if (recentQuotes.length > 0) {
// ... sync recent quotes (see refreshQuotes implementation)
lastSyncRef.current = Math.floor(Date.now() / 1000);
}
} catch (err) {
console.error('Failed to sync quote update:', err);
}
},
(err: string) => {
console.error('Quote subscription error:', err);
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.error('Sync Error: Failed to sync quotes from npub.cash');
// Option 2: Toast notification (if toast is available)
// toast({ variant: "destructive", title: "Sync Error", description: "Failed to sync quotes from npub.cash" });
// Option 3: No notification (silent failure)
}
);
return unsubscribe;
} catch (err) {
console.error('Failed to set up quote sync:', err);
return () => {};
}
}, [npcClient, getAllQuotes, getQuotesSince, subscribeToQuotes]);
```
**Usage in component:**
```typescript
// In your component
const { syncQuotesWithWallet } = useNpubCash();
const { coco } = useCashu(); // From lightning-wallet skill
useEffect(() => {
if (!coco || !isLoggedIn) return;
// Set up real-time quote syncing
const unsubscribe = await syncQuotesWithWallet(coco);
return () => {
if (unsubscribe) unsubscribe();
};
}, [coco, isLoggedIn, syncQuotesWithWallet]);
```
## Part 6: Integration with Lightning Wallet
### Optional Integration
**This skill is optional for `lightning-wallet` implementations:**
```typescript
// components/wallet/WalletHeader.tsx (excerpt)
import { useNpubCash } from '@/hooks/wallet/useNpubCash';
export function WalletHeader({ userPubkey, mode }: WalletHeaderProps) {
const { getUserInfo, isLoading } = useNpubCash();
const [npubCashUsername, setNpubCashUsername] = useState<string | null>(null);
useEffect(() => {
if (userPubkey && mode === 'lightning') {
// Optionally fetch npub.cash address
const fetchAddress = async () => {
const userInfo = await getUserInfo();
if (userInfo) {
const username = userInfo.name || extractUsernameFromPubkey(userInfo.pubkey);
setNpubCashUsername(username);
}
};
fetchAddress();
}
}, [userPubkey, mode, getUserInfo]);
return (
<div>
{/* ... other wallet header content ... */}
{/* Optionally show static address if available */}
{userPubkey && mode === 'lightning' && npubCashUsername && (
<div className="mt-3 px-4 w-full max-w-[400px] mx-auto">
<LightningAddressDisplay
lightningAddress={`${npubCashUsername}@npubx.cash`}
/>
</div>
)}
</div>
);
}
```
### Syncing Mint Settings
**Sync mint URL between wallet and npub.cash:**
```typescript
// When user changes active mint in wallet
const { setMint } = useNpubCash();
const { activeMintUrl, setActiveMintUrl } = useMintManager({ /* ... */ });
const handleMintChange = async (mintUrl: string) => {
// Update wallet mint
await setActiveMintUrl(mintUrl);
// Optionally sync to npub.cash
if (isNpubCashLoggedIn) {
await setMint(mintUrl);
}
};
```
## Part 7: Error Handling
### Authentication Errors
**Handle NIP-98 authentication failures:**
```typescript
const { getUserInfo, error, isLoggedIn } = useNpubCash();
if (!isLoggedIn) {
return (
<div>
<p>Please log in with Nostr to use npub.cash</p>
<LoginArea />
</div>
);
}
if (error) {
if (error.includes('401') || error.includes('Unauthorized')) {
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.error('Authentication Failed: Please log in again');
// Option 2: Toast notification (if toast is available)
// toast({ variant: 'destructive', title: 'Authentication Failed', description: 'Please log in again' });
// Option 3: No notification (silent failure)
}
}
```
### API Errors
**Handle npub.cash API errors:**
```typescript
try {
const userInfo = await getUserInfo();
if (!userInfo) {
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.error('Failed to Get Address: Could not retrieve your npub.cash address');
// Option 2: Toast notification (if toast is available)
// toast({ variant: 'destructive', title: 'Failed to Get Address', description: 'Could not retrieve your npub.cash address' });
// Option 3: No notification (silent failure)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
if (errorMessage.includes('404')) {
// User may not have npub.cash account yet
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.log('No Account: You may need to create an npub.cash account first');
// Option 2: Toast notification (if toast is available)
// toast({ title: 'No Account', description: 'You may need to create an npub.cash account first' });
// Option 3: No notification (silent)
} else {
// Optional: User feedback - choose one:
// Option 1: Console logging
// console.error('API Error:', errorMessage);
// Option 2: Toast notification (if toast is available)
// toast({ variant: 'destructive', title: 'Error', description: errorMessage });
// Option 3: No notification (silent failure)
}
}
```
## Part 8: Best Practices
### Lazy Loading
**Only fetch npub.cash data when needed:**
```typescript
// Don't fetch on every render
const [userInfo, setUserInfo] = useState<NpubCashUserInfo | null>(null);
const [hasFetched, setHasFetched] = useState(false);
const fetchUserInfo = useCallback(async () => {
if (hasFetched) return; // Already fetched
const info = await getUserInfo();
setUserInfo(info);
setHasFetched(true);
}, [getUserInfo, hasFetched]);
// Only fetch when user explicitly requests it or when component mounts
useEffect(() => {
if (shouldShowAddress) {
fetchUserInfo();
}
}, [shouldShowAddress, fetchUserInfo]);
```
### Caching User Info
**Cache user info to avoid repeated API calls:**
```typescript
// Use React Query or similar for caching
import { useQuery } from '@tanstack/react-query';
const { data: userInfo } = useQuery({
queryKey: ['npubcash-user-info', pubkey],
queryFn: () => getUserInfo(),
enabled: !!pubkey && isLoggedIn,
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
```
### Cleanup Subscriptions
**Always cleanup WebSocket subscriptions:**
```typescript
useEffect(() => {
if (!coco || !isLoggedIn) return;
const unsubscribe = await syncQuotesWithWallet(coco);
return () => {
if (unsubscribe) unsubscribe();
};
}, [coco, isLoggedIn, syncQuotesWithWallet]);
```
## Part 9: Common Pitfalls
### 1. ❌ Not checking authentication
**Problem:** Attempting to use npub.cash without Nostr login causes errors.
**Solution:** Always check authentication:
```typescript
if (!isLoggedIn) {
return <LoginPrompt />;
}
```
### 2. ❌ Not handling missing user info
**Problem:** Assuming user always has npub.cash account.
**Solution:** Handle null user info gracefully:
```typescript
const userInfo = await getUserInfo();
if (!userInfo) {
// User may not have npub.cash account
return <CreateAccountPrompt />;
}
```
### 3. ❌ Not cleaning up subscriptions
**Problem:** Memory leaks from uncleaned WebSocket subscriptions.
**Solution:** Always return cleanup function:
```typescript
useEffect(() => {
const unsubscribe = await syncQuotesWithWallet(coco);
return () => unsubscribe();
}, [coco, syncQuotesWithWallet]);
```
### 4. ❌ Not syncing mint settings
**Problem:** npub.cash and wallet use different mints.
**Solution:** Sync mint URL when user changes it:
```typescript
const handleMintChange = async (mintUrl: string) => {
await setActiveMintUrl(mintUrl);
if (isNpubCashLoggedIn) {
await setMint(mintUrl); // Sync to npub.cash
}
};
```
## Security Considerations
1. **NIP-98 Authentication**: Always use proper NIP-98 authentication for API calls
2. **Token Storage**: Don't store authentication tokens in localStorage
3. **Error Messages**: Don't expose internal API errors to users
4. **Quote Validation**: Validate quotes from npub.cash before adding to wallet
5. **Mint Trust**: Only trust mints from verified sources
## Verification Checklist
- [ ] NIP-98 authentication works correctly
- [ ] User info retrieval returns static address
- [ ] Static address displays correctly in UI
- [ ] Mint URL can be set and updated
- [ ] Quote syncing works (manual refresh)
- [ ] Real-time quote subscription works
- [ ] Quotes are properly added to wallet
- [ ] Unknown mints are automatically added
- [ ] WebSocket subscriptions are cleaned up
- [ ] Error handling covers all failure cases
- [ ] Integration with Lightning wallet works (optional)
## Summary
To implement npub.cash static Lightning address functionality:
1. **Set up NIP-98 authentication** - Required for npub.cash API access
2. **Initialize npubcash-sdk** - Use official SDK with JWTAuthProvider
3. **Get user info** - Retrieve static Lightning address (`username@npubx.cash`)
4. **Display address** - Show address in wallet UI with copy/QR functionality
5. **Manage settings** - Sync mint URL and lock quotes (optional)
6. **Sync quotes** - Fetch and sync quotes from npub.cash to wallet
7. **Subscribe to updates** - Use WebSocket for real-time quote updates
8. **Integrate with Lightning wallet** - Optional integration for enhanced UX
**Key principle:** This skill is for **obtaining** static Lightning addresses from npub.cash. Use the `lightning-address` skill to **resolve** any Lightning address (including npub.cash addresses) to invoices for sending payments.